Skip to main content

refget_client/
blocking.rs

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