pubmed_client/pubmed/query/
search.rs

1//! Search methods for field-specific filtering and content access
2
3use super::{ArticleType, Language, SearchQuery};
4
5impl SearchQuery {
6    /// Search in article titles only
7    ///
8    /// # Arguments
9    ///
10    /// * `title` - Title text to search for
11    ///
12    /// # Example
13    ///
14    /// ```
15    /// use pubmed_client::pubmed::SearchQuery;
16    ///
17    /// let query = SearchQuery::new()
18    ///     .title_contains("machine learning");
19    /// ```
20    pub fn title_contains<S: Into<String>>(mut self, title: S) -> Self {
21        self.filters.push(format!("{}[ti]", title.into()));
22        self
23    }
24
25    /// Search in article abstracts only
26    ///
27    /// # Arguments
28    ///
29    /// * `abstract_text` - Abstract text to search for
30    ///
31    /// # Example
32    ///
33    /// ```
34    /// use pubmed_client::pubmed::SearchQuery;
35    ///
36    /// let query = SearchQuery::new()
37    ///     .abstract_contains("deep learning neural networks");
38    /// ```
39    pub fn abstract_contains<S: Into<String>>(mut self, abstract_text: S) -> Self {
40        self.filters.push(format!("{}[tiab]", abstract_text.into()));
41        self
42    }
43
44    /// Search in both title and abstract
45    ///
46    /// # Arguments
47    ///
48    /// * `text` - Text to search for in title or abstract
49    ///
50    /// # Example
51    ///
52    /// ```
53    /// use pubmed_client::pubmed::SearchQuery;
54    ///
55    /// let query = SearchQuery::new()
56    ///     .title_or_abstract("CRISPR gene editing");
57    /// ```
58    pub fn title_or_abstract<S: Into<String>>(mut self, text: S) -> Self {
59        self.filters.push(format!("{}[tiab]", text.into()));
60        self
61    }
62
63    /// Filter by journal name
64    ///
65    /// # Arguments
66    ///
67    /// * `journal` - Journal name to search for
68    ///
69    /// # Example
70    ///
71    /// ```
72    /// use pubmed_client::pubmed::SearchQuery;
73    ///
74    /// let query = SearchQuery::new()
75    ///     .query("cancer treatment")
76    ///     .journal("Nature");
77    /// ```
78    pub fn journal<S: Into<String>>(mut self, journal: S) -> Self {
79        self.filters.push(format!("{}[ta]", journal.into()));
80        self
81    }
82
83    /// Filter by journal title abbreviation
84    ///
85    /// # Arguments
86    ///
87    /// * `abbreviation` - Journal abbreviation to search for
88    ///
89    /// # Example
90    ///
91    /// ```
92    /// use pubmed_client::pubmed::SearchQuery;
93    ///
94    /// let query = SearchQuery::new()
95    ///     .query("stem cells")
96    ///     .journal_abbreviation("Nat Med");
97    /// ```
98    pub fn journal_abbreviation<S: Into<String>>(mut self, abbreviation: S) -> Self {
99        self.filters.push(format!("{}[ta]", abbreviation.into()));
100        self
101    }
102
103    /// Filter by grant number
104    ///
105    /// # Arguments
106    ///
107    /// * `grant_number` - Grant number to search for
108    ///
109    /// # Example
110    ///
111    /// ```
112    /// use pubmed_client::pubmed::SearchQuery;
113    ///
114    /// let query = SearchQuery::new()
115    ///     .grant_number("R01AI123456");
116    /// ```
117    pub fn grant_number<S: Into<String>>(mut self, grant_number: S) -> Self {
118        self.filters.push(format!("{}[gr]", grant_number.into()));
119        self
120    }
121
122    /// Filter by ISBN
123    ///
124    /// # Arguments
125    ///
126    /// * `isbn` - ISBN to search for
127    ///
128    /// # Example
129    ///
130    /// ```
131    /// use pubmed_client::pubmed::SearchQuery;
132    ///
133    /// let query = SearchQuery::new()
134    ///     .isbn("978-0123456789");
135    /// ```
136    pub fn isbn<S: Into<String>>(mut self, isbn: S) -> Self {
137        self.filters.push(format!("{}[ISBN]", isbn.into()));
138        self
139    }
140
141    /// Filter by ISSN
142    ///
143    /// # Arguments
144    ///
145    /// * `issn` - ISSN to search for
146    ///
147    /// # Example
148    ///
149    /// ```
150    /// use pubmed_client::pubmed::SearchQuery;
151    ///
152    /// let query = SearchQuery::new()
153    ///     .issn("1234-5678");
154    /// ```
155    pub fn issn<S: Into<String>>(mut self, issn: S) -> Self {
156        self.filters.push(format!("{}[ISSN]", issn.into()));
157        self
158    }
159
160    /// Filter to articles with free full text only
161    ///
162    /// Includes PMC, Bookshelf, and publishers' websites.
163    ///
164    /// # Example
165    ///
166    /// ```
167    /// use pubmed_client::pubmed::SearchQuery;
168    ///
169    /// let query = SearchQuery::new()
170    ///     .query("cancer")
171    ///     .free_full_text_only();
172    /// ```
173    pub fn free_full_text_only(mut self) -> Self {
174        self.filters.push("free full text[sb]".to_string());
175        self
176    }
177
178    /// Filter to articles with full text links (including subscription-based)
179    ///
180    /// # Example
181    ///
182    /// ```
183    /// use pubmed_client::pubmed::SearchQuery;
184    ///
185    /// let query = SearchQuery::new()
186    ///     .query("machine learning")
187    ///     .full_text_only();
188    /// ```
189    pub fn full_text_only(mut self) -> Self {
190        self.filters.push("full text[sb]".to_string());
191        self
192    }
193
194    /// Filter to articles with PMC full text only
195    ///
196    /// # Example
197    ///
198    /// ```
199    /// use pubmed_client::pubmed::SearchQuery;
200    ///
201    /// let query = SearchQuery::new()
202    ///     .query("diabetes")
203    ///     .pmc_only();
204    /// ```
205    pub fn pmc_only(mut self) -> Self {
206        self.filters.push("pubmed pmc[sb]".to_string());
207        self
208    }
209
210    /// Filter to articles with abstracts
211    ///
212    /// # Example
213    ///
214    /// ```
215    /// use pubmed_client::pubmed::SearchQuery;
216    ///
217    /// let query = SearchQuery::new()
218    ///     .query("genetics")
219    ///     .has_abstract();
220    /// ```
221    pub fn has_abstract(mut self) -> Self {
222        self.filters.push("hasabstract".to_string());
223        self
224    }
225
226    /// Filter by article types
227    ///
228    /// # Arguments
229    ///
230    /// * `types` - Article types to include
231    ///
232    /// # Example
233    ///
234    /// ```
235    /// use pubmed_client::pubmed::{SearchQuery, ArticleType};
236    ///
237    /// let query = SearchQuery::new()
238    ///     .query("hypertension")
239    ///     .article_types(&[ArticleType::ClinicalTrial, ArticleType::Review]);
240    /// ```
241    pub fn article_types(mut self, types: &[ArticleType]) -> Self {
242        if !types.is_empty() {
243            let type_filters: Vec<String> = types
244                .iter()
245                .map(|t| t.to_query_string().to_string())
246                .collect();
247
248            if type_filters.len() == 1 {
249                self.filters.push(type_filters[0].clone());
250            } else {
251                // Multiple types: (type1[pt] OR type2[pt] OR ...)
252                let combined = format!("({})", type_filters.join(" OR "));
253                self.filters.push(combined);
254            }
255        }
256        self
257    }
258
259    /// Filter by a single article type (convenience method)
260    ///
261    /// # Arguments
262    ///
263    /// * `article_type` - Article type to filter by
264    ///
265    /// # Example
266    ///
267    /// ```
268    /// use pubmed_client::pubmed::{SearchQuery, ArticleType};
269    ///
270    /// let query = SearchQuery::new()
271    ///     .query("diabetes treatment")
272    ///     .article_type(ArticleType::ClinicalTrial);
273    /// ```
274    pub fn article_type(self, article_type: ArticleType) -> Self {
275        self.article_types(&[article_type])
276    }
277
278    /// Filter by language
279    ///
280    /// # Arguments
281    ///
282    /// * `language` - Language to filter by
283    ///
284    /// # Example
285    ///
286    /// ```
287    /// use pubmed_client::pubmed::{SearchQuery, Language};
288    ///
289    /// let query = SearchQuery::new()
290    ///     .query("stem cells")
291    ///     .language(Language::English);
292    /// ```
293    pub fn language(mut self, language: Language) -> Self {
294        self.filters.push(language.to_query_string());
295        self
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_title_contains() {
305        let query = SearchQuery::new().title_contains("machine learning");
306        assert_eq!(query.build(), "machine learning[ti]");
307    }
308
309    #[test]
310    fn test_abstract_contains() {
311        let query = SearchQuery::new().abstract_contains("deep learning neural networks");
312        assert_eq!(query.build(), "deep learning neural networks[tiab]");
313    }
314
315    #[test]
316    fn test_title_or_abstract() {
317        let query = SearchQuery::new().title_or_abstract("CRISPR gene editing");
318        assert_eq!(query.build(), "CRISPR gene editing[tiab]");
319    }
320
321    #[test]
322    fn test_journal() {
323        let query = SearchQuery::new().journal("Nature");
324        assert_eq!(query.build(), "Nature[ta]");
325    }
326
327    #[test]
328    fn test_journal_abbreviation() {
329        let query = SearchQuery::new().journal_abbreviation("Nat Med");
330        assert_eq!(query.build(), "Nat Med[ta]");
331    }
332
333    #[test]
334    fn test_grant_number() {
335        let query = SearchQuery::new().grant_number("R01AI123456");
336        assert_eq!(query.build(), "R01AI123456[gr]");
337    }
338
339    #[test]
340    fn test_isbn() {
341        let query = SearchQuery::new().isbn("978-0123456789");
342        assert_eq!(query.build(), "978-0123456789[ISBN]");
343    }
344
345    #[test]
346    fn test_issn() {
347        let query = SearchQuery::new().issn("1234-5678");
348        assert_eq!(query.build(), "1234-5678[ISSN]");
349    }
350
351    #[test]
352    fn test_free_full_text_only() {
353        let query = SearchQuery::new().free_full_text_only();
354        assert_eq!(query.build(), "free full text[sb]");
355    }
356
357    #[test]
358    fn test_full_text_only() {
359        let query = SearchQuery::new().full_text_only();
360        assert_eq!(query.build(), "full text[sb]");
361    }
362
363    #[test]
364    fn test_pmc_only() {
365        let query = SearchQuery::new().pmc_only();
366        assert_eq!(query.build(), "pubmed pmc[sb]");
367    }
368
369    #[test]
370    fn test_has_abstract() {
371        let query = SearchQuery::new().has_abstract();
372        assert_eq!(query.build(), "hasabstract");
373    }
374
375    #[test]
376    fn test_single_article_type() {
377        let query = SearchQuery::new().article_type(ArticleType::ClinicalTrial);
378        assert_eq!(query.build(), "Clinical Trial[pt]");
379    }
380
381    #[test]
382    fn test_multiple_article_types() {
383        let types = [ArticleType::ClinicalTrial, ArticleType::Review];
384        let query = SearchQuery::new().article_types(&types);
385        assert_eq!(query.build(), "(Clinical Trial[pt] OR Review[pt])");
386    }
387
388    #[test]
389    fn test_empty_article_types() {
390        let types: &[ArticleType] = &[];
391        let query = SearchQuery::new().article_types(types);
392        assert_eq!(query.build(), "");
393    }
394
395    #[test]
396    fn test_single_article_type_via_array() {
397        let types = [ArticleType::Review];
398        let query = SearchQuery::new().article_types(&types);
399        assert_eq!(query.build(), "Review[pt]");
400    }
401
402    #[test]
403    fn test_language() {
404        let query = SearchQuery::new().language(Language::English);
405        assert_eq!(query.build(), "English[la]");
406    }
407
408    #[test]
409    fn test_language_other() {
410        let query = SearchQuery::new().language(Language::Other("Esperanto".to_string()));
411        assert_eq!(query.build(), "Esperanto[la]");
412    }
413
414    #[test]
415    fn test_combined_search_filters() {
416        let query = SearchQuery::new()
417            .query("cancer treatment")
418            .title_contains("immunotherapy")
419            .journal("Nature")
420            .free_full_text_only()
421            .article_type(ArticleType::ClinicalTrial)
422            .language(Language::English);
423
424        let expected = "cancer treatment AND immunotherapy[ti] AND Nature[ta] AND free full text[sb] AND Clinical Trial[pt] AND English[la]";
425        assert_eq!(query.build(), expected);
426    }
427
428    #[test]
429    fn test_multiple_journal_filters() {
430        let query = SearchQuery::new().journal("Nature").journal("Science");
431        assert_eq!(query.build(), "Nature[ta] AND Science[ta]");
432    }
433
434    #[test]
435    fn test_title_and_abstract_separate() {
436        let query = SearchQuery::new()
437            .title_contains("machine learning")
438            .abstract_contains("neural networks");
439        assert_eq!(
440            query.build(),
441            "machine learning[ti] AND neural networks[tiab]"
442        );
443    }
444
445    #[test]
446    fn test_all_text_availability_filters() {
447        let query = SearchQuery::new()
448            .query("research")
449            .has_abstract()
450            .full_text_only()
451            .free_full_text_only()
452            .pmc_only();
453        assert_eq!(
454            query.build(),
455            "research AND hasabstract AND full text[sb] AND free full text[sb] AND pubmed pmc[sb]"
456        );
457    }
458
459    #[test]
460    fn test_many_article_types() {
461        let types = [
462            ArticleType::ClinicalTrial,
463            ArticleType::Review,
464            ArticleType::MetaAnalysis,
465            ArticleType::SystematicReview,
466        ];
467        let query = SearchQuery::new().article_types(&types);
468        let expected =
469            "(Clinical Trial[pt] OR Review[pt] OR Meta-Analysis[pt] OR Systematic Review[pt])";
470        assert_eq!(query.build(), expected);
471    }
472
473    #[test]
474    fn test_identifier_fields() {
475        let query = SearchQuery::new()
476            .grant_number("R01CA123456")
477            .isbn("978-0123456789")
478            .issn("0028-0836");
479
480        let expected = "R01CA123456[gr] AND 978-0123456789[ISBN] AND 0028-0836[ISSN]";
481        assert_eq!(query.build(), expected);
482    }
483}