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