Skip to main content

refget_client/
async_client.rs

1//! Async HTTP client for the refget API.
2
3use refget_model::{ComparisonResult, SeqCol, SeqColLevel1, SequenceMetadata, SequenceServiceInfo};
4use reqwest::{Client, RequestBuilder, Response, StatusCode};
5use serde_json::Value;
6
7use crate::error::{ClientError, ClientResult};
8use crate::response::{self, MetadataResponse};
9
10/// Async HTTP client for a refget server.
11///
12/// Supports the GA4GH refget Sequences v2.0.0 and Sequence Collections v1.0.0 APIs.
13pub struct RefgetClient {
14    client: Client,
15    base_url: String,
16}
17
18impl RefgetClient {
19    /// Create a new client with a default `reqwest::Client`.
20    pub fn new(base_url: &str) -> ClientResult<Self> {
21        let client = Client::builder().build().map_err(ClientError::Http)?;
22        Self::with_client(client, base_url)
23    }
24
25    /// Create a new client with a pre-configured `reqwest::Client`.
26    pub fn with_client(client: Client, base_url: &str) -> ClientResult<Self> {
27        if base_url.is_empty() {
28            return Err(ClientError::InvalidUrl("base URL must not be empty".to_string()));
29        }
30        Ok(Self { client, base_url: base_url.trim_end_matches('/').to_string() })
31    }
32
33    /// Send a request and handle the response status.
34    ///
35    /// Returns `Ok(Some(response))` for 2xx, `Ok(None)` for 404,
36    /// and `Err(ServerError)` for other non-2xx statuses.
37    async fn send_optional(&self, req: RequestBuilder) -> ClientResult<Option<Response>> {
38        let resp = req.send().await?;
39        let status = resp.status();
40        if status.is_success() {
41            Ok(Some(resp))
42        } else if status == StatusCode::NOT_FOUND {
43            Ok(None)
44        } else {
45            let body = resp.text().await.unwrap_or_default();
46            Err(ClientError::ServerError { status: status.as_u16(), body })
47        }
48    }
49
50    /// Send a request that must succeed (404 is an error, not `None`).
51    async fn send_required(&self, req: RequestBuilder) -> ClientResult<Response> {
52        let resp = req.send().await?;
53        if resp.status().is_success() {
54            Ok(resp)
55        } else {
56            let status = resp.status().as_u16();
57            let body = resp.text().await.unwrap_or_default();
58            Err(ClientError::ServerError { status, body })
59        }
60    }
61
62    // --- Sequences API ---
63
64    /// Retrieve a sequence (or subsequence) by digest.
65    ///
66    /// With `start` and/or `end` set, retrieves a subsequence (0-based, half-open).
67    /// The server handles defaulting omitted bounds.
68    pub async fn get_sequence(
69        &self,
70        digest: &str,
71        start: Option<u64>,
72        end: Option<u64>,
73    ) -> ClientResult<Option<Vec<u8>>> {
74        let mut req = self.client.get(format!("{}/sequence/{digest}", self.base_url));
75        if let Some(s) = start {
76            req = req.query(&[("start", s)]);
77        }
78        if let Some(e) = end {
79            req = req.query(&[("end", e)]);
80        }
81        match self.send_optional(req).await? {
82            Some(resp) => Ok(Some(resp.bytes().await?.to_vec())),
83            None => Ok(None),
84        }
85    }
86
87    /// Retrieve metadata for a sequence by digest.
88    pub async fn get_metadata(&self, digest: &str) -> ClientResult<Option<SequenceMetadata>> {
89        let req = self.client.get(format!("{}/sequence/{digest}/metadata", self.base_url));
90        match self.send_optional(req).await? {
91            Some(resp) => {
92                let envelope: MetadataResponse = resp.json().await?;
93                Ok(Some(envelope.metadata))
94            }
95            None => Ok(None),
96        }
97    }
98
99    /// Retrieve the sequence service-info.
100    pub async fn get_sequence_service_info(&self) -> ClientResult<SequenceServiceInfo> {
101        let req = self.client.get(format!("{}/sequence/service-info", self.base_url));
102        let resp = self.send_required(req).await?;
103        let value: Value = resp.json().await?;
104        response::deserialize_sequence_service_info(value).map_err(ClientError::Deserialize)
105    }
106
107    // --- Sequence Collections API ---
108
109    /// Retrieve a collection at Level 0 (single digest string).
110    pub async fn get_collection_level0(&self, digest: &str) -> ClientResult<Option<String>> {
111        let req = self
112            .client
113            .get(format!("{}/collection/{digest}", self.base_url))
114            .query(&[("level", "0")]);
115        match self.send_optional(req).await? {
116            Some(resp) => {
117                let value: Value = resp.json().await?;
118                match value.as_str() {
119                    Some(s) => Ok(Some(s.to_string())),
120                    None => Ok(Some(value.to_string())),
121                }
122            }
123            None => Ok(None),
124        }
125    }
126
127    /// Retrieve a collection at Level 1 (per-attribute digests).
128    pub async fn get_collection_level1(&self, digest: &str) -> ClientResult<Option<SeqColLevel1>> {
129        let req = self
130            .client
131            .get(format!("{}/collection/{digest}", self.base_url))
132            .query(&[("level", "1")]);
133        match self.send_optional(req).await? {
134            Some(resp) => Ok(Some(resp.json().await?)),
135            None => Ok(None),
136        }
137    }
138
139    /// Retrieve a collection at Level 2 (full arrays).
140    pub async fn get_collection_level2(&self, digest: &str) -> ClientResult<Option<SeqCol>> {
141        let req = self
142            .client
143            .get(format!("{}/collection/{digest}", self.base_url))
144            .query(&[("level", "2")]);
145        match self.send_optional(req).await? {
146            Some(resp) => Ok(Some(resp.json().await?)),
147            None => Ok(None),
148        }
149    }
150
151    /// Retrieve a collection at an arbitrary level as raw JSON.
152    pub async fn get_collection_raw(&self, digest: &str, level: u8) -> ClientResult<Option<Value>> {
153        let req = self
154            .client
155            .get(format!("{}/collection/{digest}", self.base_url))
156            .query(&[("level", level.to_string())]);
157        match self.send_optional(req).await? {
158            Some(resp) => Ok(Some(resp.json().await?)),
159            None => Ok(None),
160        }
161    }
162
163    /// Compare two collections by their digests.
164    pub async fn compare_collections(
165        &self,
166        digest_a: &str,
167        digest_b: &str,
168    ) -> ClientResult<ComparisonResult> {
169        let req = self.client.get(format!("{}/comparison/{digest_a}/{digest_b}", self.base_url));
170        Ok(self.send_required(req).await?.json().await?)
171    }
172
173    /// Compare a stored collection (by digest) with a provided collection via POST.
174    pub async fn compare_collection_with(
175        &self,
176        digest: &str,
177        collection: &SeqCol,
178    ) -> ClientResult<ComparisonResult> {
179        let req =
180            self.client.post(format!("{}/comparison/{digest}", self.base_url)).json(collection);
181        Ok(self.send_required(req).await?.json().await?)
182    }
183
184    /// List collections with optional filters and pagination.
185    ///
186    /// Returns the raw JSON response which includes `items`, `total`, `page`, and `page_size`.
187    pub async fn list_collections(
188        &self,
189        filters: &[(&str, &str)],
190        page: usize,
191        page_size: usize,
192    ) -> ClientResult<Value> {
193        let mut req = self
194            .client
195            .get(format!("{}/list/collection", self.base_url))
196            .query(&[("page", page.to_string()), ("page_size", page_size.to_string())]);
197        for (key, value) in filters {
198            req = req.query(&[(key, value)]);
199        }
200        Ok(self.send_required(req).await?.json().await?)
201    }
202
203    /// Get a single attribute array by attribute name and its digest.
204    pub async fn get_attribute(&self, attr: &str, digest: &str) -> ClientResult<Option<Value>> {
205        let req =
206            self.client.get(format!("{}/attribute/collection/{attr}/{digest}", self.base_url));
207        match self.send_optional(req).await? {
208            Some(resp) => Ok(Some(resp.json().await?)),
209            None => Ok(None),
210        }
211    }
212
213    /// Get the sequence collections service-info as raw JSON.
214    pub async fn get_seqcol_service_info(&self) -> ClientResult<Value> {
215        let req = self.client.get(format!("{}/service-info", self.base_url));
216        Ok(self.send_required(req).await?.json().await?)
217    }
218}