pubmed_client/pubmed/client/
einfo.rs

1//! EInfo API operations for retrieving NCBI database information
2
3use crate::error::{PubMedError, Result};
4use crate::pubmed::models::{DatabaseInfo, FieldInfo, LinkInfo};
5use crate::pubmed::responses::EInfoResponse;
6use tracing::{debug, info, instrument};
7
8use super::PubMedClient;
9
10impl PubMedClient {
11    /// Get list of all available NCBI databases
12    ///
13    /// # Returns
14    ///
15    /// Returns a `Result<Vec<String>>` containing names of all available databases
16    ///
17    /// # Errors
18    ///
19    /// * `PubMedError::RequestError` - If the HTTP request fails
20    /// * `ParseError::JsonError` - If JSON parsing fails
21    ///
22    /// # Example
23    ///
24    /// ```no_run
25    /// use pubmed_client::PubMedClient;
26    ///
27    /// #[tokio::main]
28    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
29    ///     let client = PubMedClient::new();
30    ///     let databases = client.get_database_list().await?;
31    ///     println!("Available databases: {:?}", databases);
32    ///     Ok(())
33    /// }
34    /// ```
35    #[instrument(skip(self))]
36    pub async fn get_database_list(&self) -> Result<Vec<String>> {
37        // Build URL - API parameters will be added by make_request
38        let url = format!("{}/einfo.fcgi?retmode=json", self.base_url);
39
40        debug!("Making EInfo API request for database list");
41        let response = self.make_request(&url).await?;
42
43        let einfo_response: EInfoResponse = response.json().await?;
44
45        let db_list = einfo_response.einfo_result.db_list.unwrap_or_default();
46
47        info!(
48            databases_found = db_list.len(),
49            "Database list retrieved successfully"
50        );
51
52        Ok(db_list)
53    }
54
55    /// Get detailed information about a specific database
56    ///
57    /// # Arguments
58    ///
59    /// * `database` - Name of the database (e.g., "pubmed", "pmc", "books")
60    ///
61    /// # Returns
62    ///
63    /// Returns a `Result<DatabaseInfo>` containing detailed database information
64    ///
65    /// # Errors
66    ///
67    /// * `PubMedError::RequestError` - If the HTTP request fails
68    /// * `ParseError::JsonError` - If JSON parsing fails
69    /// * `PubMedError::ApiError` - If the database doesn't exist
70    ///
71    /// # Example
72    ///
73    /// ```no_run
74    /// use pubmed_client::PubMedClient;
75    ///
76    /// #[tokio::main]
77    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
78    ///     let client = PubMedClient::new();
79    ///     let db_info = client.get_database_info("pubmed").await?;
80    ///     println!("Database: {}", db_info.name);
81    ///     println!("Description: {}", db_info.description);
82    ///     println!("Fields: {}", db_info.fields.len());
83    ///     Ok(())
84    /// }
85    /// ```
86    #[instrument(skip(self), fields(database = %database))]
87    pub async fn get_database_info(&self, database: &str) -> Result<DatabaseInfo> {
88        if database.trim().is_empty() {
89            return Err(PubMedError::ApiError {
90                status: 400,
91                message: "Database name cannot be empty".to_string(),
92            });
93        }
94
95        // Build URL - API parameters will be added by make_request
96        let url = format!(
97            "{}/einfo.fcgi?db={}&retmode=json",
98            self.base_url,
99            urlencoding::encode(database)
100        );
101
102        debug!("Making EInfo API request for database details");
103        let response = self.make_request(&url).await?;
104
105        let einfo_response: EInfoResponse = response.json().await?;
106
107        let db_info_list =
108            einfo_response
109                .einfo_result
110                .db_info
111                .ok_or_else(|| PubMedError::ApiError {
112                    status: 404,
113                    message: format!("Database '{database}' not found or no information available"),
114                })?;
115
116        let db_info = db_info_list
117            .into_iter()
118            .next()
119            .ok_or_else(|| PubMedError::ApiError {
120                status: 404,
121                message: format!("Database '{database}' information not found"),
122            })?;
123
124        // Convert internal response to public model
125        let fields = db_info
126            .field_list
127            .unwrap_or_default()
128            .into_iter()
129            .map(|field| FieldInfo {
130                name: field.name,
131                full_name: field.full_name,
132                description: field.description,
133                term_count: field.term_count.and_then(|s| s.parse().ok()),
134                is_date: field.is_date.as_deref() == Some("Y"),
135                is_numerical: field.is_numerical.as_deref() == Some("Y"),
136                single_token: field.single_token.as_deref() == Some("Y"),
137                hierarchy: field.hierarchy.as_deref() == Some("Y"),
138                is_hidden: field.is_hidden.as_deref() == Some("Y"),
139            })
140            .collect();
141
142        let links = db_info
143            .link_list
144            .unwrap_or_default()
145            .into_iter()
146            .map(|link| LinkInfo {
147                name: link.name,
148                menu: link.menu,
149                description: link.description,
150                target_db: link.db_to,
151            })
152            .collect();
153
154        let database_info = DatabaseInfo {
155            name: db_info.db_name,
156            menu_name: db_info.menu_name,
157            description: db_info.description,
158            build: db_info.db_build,
159            count: db_info.count.and_then(|s| s.parse().ok()),
160            last_update: db_info.last_update,
161            fields,
162            links,
163        };
164
165        info!(
166            fields_count = database_info.fields.len(),
167            links_count = database_info.links.len(),
168            "Database information retrieved successfully"
169        );
170
171        Ok(database_info)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::config::ClientConfig;
179
180    #[test]
181    fn test_empty_database_name_validation() {
182        use tokio_test;
183
184        let config = ClientConfig::new();
185        let client = PubMedClient::with_config(config);
186
187        let result = tokio_test::block_on(client.get_database_info(""));
188        assert!(result.is_err());
189
190        if let Err(e) = result {
191            assert!(e.to_string().contains("empty"));
192        }
193    }
194
195    #[test]
196    fn test_whitespace_database_name_validation() {
197        use tokio_test;
198
199        let config = ClientConfig::new();
200        let client = PubMedClient::with_config(config);
201
202        let result = tokio_test::block_on(client.get_database_info("   "));
203        assert!(result.is_err());
204
205        if let Err(e) = result {
206            assert!(e.to_string().contains("empty"));
207        }
208    }
209}