wiki_api/
search.rs

1use anyhow::{anyhow, Context, Result};
2
3use bitflags::bitflags;
4use core::fmt;
5use reqwest::{Client, Response};
6use scraper::Html;
7use serde::Deserialize;
8use serde_repr::{Deserialize_repr, Serialize_repr};
9use std::fmt::Debug;
10use std::fmt::Display;
11use std::fmt::Write;
12
13use crate::Endpoint;
14
15use crate::languages::Language;
16
17/// A finished search containing the found results and additional optional information regarding
18/// the search
19#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub struct Search {
21    /// The found results in this batch
22    pub results: Vec<SearchResult>,
23    /// API endpoint of the MediaWiki site where the search was performed on
24    pub endpoint: Endpoint,
25    /// If more results are available, use this offset to continue the search
26    pub continue_offset: Option<usize>,
27    /// General information about the search
28    pub info: SearchInfo,
29}
30
31impl Search {
32    /// Creates a [`SearchBuilder`] to configure and perform a search
33    ///
34    /// [`SearchBuilder`]: SearchBuilder
35    pub fn builder() -> SearchBuilder<NoQuery, NoEndpoint, NoLanguage> {
36        SearchBuilder::default()
37    }
38
39    /// If available, returns the the data necessary for continuing the current search
40    ///
41    /// When are more results available for the search, which can be checked via the
42    /// `Search::complete` field, creates a [`SearchContinue`] data
43    /// struct that contains all of the necessary information to continue the search at the
44    /// correct offset.
45    ///
46    /// [`SearchContinue`]: SearchContinue
47    pub fn continue_data(&self) -> Option<SearchContinue> {
48        if let Some(ref offset) = self.continue_offset {
49            let info: &SearchInfo = &self.info;
50            return Some(SearchContinue {
51                query: info.query.clone(),
52                endpoint: self.endpoint.clone(),
53                language: info.language,
54                offset: *offset,
55            });
56        }
57        None
58    }
59}
60
61impl Debug for Search {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("Search")
64            .field("continue_offset", &self.continue_offset)
65            .field("results", &self.results.len())
66            .field("info", &self.info)
67            .finish()
68    }
69}
70
71/// Contains general informations about the search
72#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
73pub struct SearchInfo {
74    /// Whether the search is complete and no more results are available
75    pub complete: bool,
76    /// Optional: Total amount of results found
77    pub total_hits: Option<usize>,
78    /// Optional: Suggestion for a different query
79    pub suggestion: Option<String>,
80    /// Optional: The query rewritten by the search backend (See [`SearchBuilder::rewrites`] for
81    /// more)
82    ///
83    /// [`SearchBuilder::rewrites`]: SearchBuilder::rewrites
84    pub rewritten_query: Option<String>,
85    /// Searched value
86    pub query: String,
87    /// In what language the search was made
88    pub language: Language,
89}
90
91/// Contains the necessary data for continuing a Search at a given offset. This data can be
92/// extracted from a already existing search with [`Search::continue_data`]
93///
94/// # Example
95///
96/// ```
97/// // This will continue the already completed search
98/// let continue_data = search.continue_data()?;
99/// let continued_search = Search::builder()
100///     .query(continue_data.query)
101///     .endpoint(continue_data.endpoint)
102///     .langauge(continue_data.language)
103///     .offset(continue_data.offset)
104///     .search()?;
105/// ```
106///
107/// [`Search::continue_data`]: Search::continue_data
108#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
109pub struct SearchContinue {
110    // TODO: SearchContinue::continue() creates a Search::builder() and continues the search
111    // WARN: Do the properties and settings still exist?
112    /// Search for page titles or content matching this value
113    pub query: String,
114    /// API endpoint of the MediaWiki site to perform the search on
115    pub endpoint: Endpoint,
116    /// In what language to perform the search
117    pub language: Language,
118    /// Offset where the search will continue
119    pub offset: usize,
120}
121
122/// A single search result containing additional optional properties if they were added in the
123/// search
124#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
125pub struct SearchResult {
126    /// Namespace where the page belongs to
127    pub namespace: Namespace,
128    /// Title of the page
129    pub title: String,
130    /// PageId of the page
131    pub pageid: usize,
132
133    /// Language, the page is written in
134    pub language: Language,
135    /// API endpoint of the MediaWiki site this page belongs to
136    pub endpoint: Endpoint,
137
138    /// Optional: Size in bytes of the page
139    pub size: Option<usize>,
140    /// Optional: Word count of the page
141    pub wordcount: Option<usize>,
142    /// Optional: Snippet of the page, with query term highlighting markup
143    pub snippet: Option<String>,
144    /// Optional: Timestamp of when the page was last edited
145    pub timestamp: Option<String>,
146}
147
148impl SearchResult {
149    pub fn cleaned_snippet(&self) -> String {
150        self.snippet
151            .as_ref()
152            .map(|snip| {
153                let fragment = Html::parse_document(snip);
154                fragment.root_element().text().collect()
155            })
156            .unwrap_or_default()
157    }
158}
159
160/// The 16 built-in namespaces (excluding two "virtual" namespaces) of MediaWiki
161///
162/// A namespace is a collection of pages which have content with a similar purposek, i. e. pages
163/// where the intended use is the same. Namespaces can be thought of as partitions of different
164/// types of information within the same wiki, and keep "real" content separate from user profiles,
165/// help pages, etc.
166///
167/// These are the 16 built-in "real" namespaces, meaning namespaces corresponding to actual pages.
168/// They each have a unique number (0 to 15) and are grouped in subject/talk pairs
169///
170/// Read more in the [MediaWiki API docs](https://www.mediawiki.org/wiki/Manual:Namespace)
171#[derive(Serialize_repr, Deserialize_repr, Debug, Clone, PartialEq, Eq)]
172#[repr(usize)]
173pub enum Namespace {
174    Main = 0,
175    MainTalk = 1,
176    User = 2,
177    UserTalk = 3,
178    Project = 4,
179    ProjectTalk = 5,
180    File = 6,
181    FileTalk = 7,
182    MediaWiki = 8,
183    MediaWikiTalk = 9,
184    Template = 10,
185    TemplateTalk = 11,
186    Help = 12,
187    HelpTalk = 13,
188    Category = 14,
189    CategoryTalk = 15,
190}
191
192impl Display for Namespace {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        match self {
195            Namespace::Main => write!(f, "Main"),
196            Namespace::MainTalk => write!(f, "Main_talk"),
197            Namespace::User => write!(f, "User"),
198            Namespace::UserTalk => write!(f, "User_talk"),
199            Namespace::Project => write!(f, "Project"),
200            Namespace::ProjectTalk => write!(f, "Project_talk"),
201            Namespace::File => write!(f, "File"),
202            Namespace::FileTalk => write!(f, "File_talk"),
203            Namespace::MediaWiki => write!(f, "Mediawiki"),
204            Namespace::MediaWikiTalk => write!(f, "Mediawiki_talk"),
205            Namespace::Template => write!(f, "Template"),
206            Namespace::TemplateTalk => write!(f, "Template_talk"),
207            Namespace::Help => write!(f, "Help"),
208            Namespace::HelpTalk => write!(f, "Help_talk"),
209            Namespace::Category => write!(f, "Category"),
210            Namespace::CategoryTalk => write!(f, "Category_talk"),
211        }
212    }
213}
214
215impl Namespace {
216    pub fn from_string(namespace: &str) -> Option<Namespace> {
217        match namespace.to_lowercase().as_str() {
218            "main" => Some(Namespace::Main),
219            "main_talk" => Some(Namespace::MainTalk),
220            "user" => Some(Namespace::User),
221            "user_talk" => Some(Namespace::UserTalk),
222            "project" => Some(Namespace::Project),
223            "project_talk" => Some(Namespace::ProjectTalk),
224            "file" => Some(Namespace::File),
225            "file_talk" => Some(Namespace::FileTalk),
226            "mediawiki" => Some(Namespace::MediaWiki),
227            "mediawiki_talk" => Some(Namespace::MediaWikiTalk),
228            "template" => Some(Namespace::Template),
229            "template_talk" => Some(Namespace::TemplateTalk),
230            "help" => Some(Namespace::Help),
231            "help_talk" => Some(Namespace::HelpTalk),
232            "category" => Some(Namespace::Category),
233            "category_talk" => Some(Namespace::CategoryTalk),
234            _ => None,
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::Namespace;
242    #[test]
243    fn test_namespace_display_and_str() {
244        macro_rules! test_namespace {
245            ($namespace: ident, $namespace_talk: ident) => {
246                let namespace_str = format!("{}", Namespace::$namespace);
247                assert_eq!(
248                    Namespace::from_string(&namespace_str),
249                    Some(Namespace::$namespace)
250                );
251
252                let namespace_str = format!("{}", Namespace::$namespace_talk);
253                assert_eq!(
254                    Namespace::from_string(&namespace_str),
255                    Some(Namespace::$namespace_talk)
256                );
257            };
258        }
259
260        test_namespace!(Main, MainTalk);
261        test_namespace!(User, UserTalk);
262        test_namespace!(Project, ProjectTalk);
263        test_namespace!(File, FileTalk);
264        test_namespace!(MediaWiki, MediaWikiTalk);
265        test_namespace!(Template, TemplateTalk);
266        test_namespace!(Help, HelpTalk);
267        test_namespace!(Category, CategoryTalk);
268    }
269}
270
271/// Query independent profile which affects the ranking algorithm
272#[derive(Default, Clone, Deserialize, serde::Serialize)]
273#[serde(rename_all = "lowercase")]
274pub enum QiProfile {
275    /// Ranking based on the number of incoming links, some templates, page language and recency
276    /// (templates / language / recency may not be activated on the wiki where the search is
277    /// performed on)
278    Classic,
279    /// Ranking based on some templates, page language and recency when activated on the wiki where
280    /// the search is performed on
281    ClassicNoBoostLinks,
282    /// Weighted sum based on incoming links
283    WSumIncLinks,
284    /// Weighted sum based on incoming links and weekly pageviews
285    WSumIncLinksPV,
286    /// Ranking based primarily on page views
287    PopularIncLinksPV,
288    /// Ranking based primarily on incoming link counts
289    PopularIncLinks,
290    /// Let the search engine decide on the best profile to use
291    #[default]
292    EngineAutoselect,
293}
294
295impl Display for QiProfile {
296    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297        match self {
298            QiProfile::Classic => write!(f, "classic"),
299            QiProfile::ClassicNoBoostLinks => write!(f, "classic_noboostlinks"),
300            QiProfile::WSumIncLinks => write!(f, "wsum_inclinks"),
301            QiProfile::WSumIncLinksPV => write!(f, "wsum_inclinks_pv"),
302            QiProfile::PopularIncLinksPV => write!(f, "popular_inclinks_pv"),
303            QiProfile::PopularIncLinks => write!(f, "popular_inclinks"),
304            QiProfile::EngineAutoselect => write!(f, "engine_autoselect"),
305        }
306    }
307}
308
309/// The type of search
310#[derive(Default, Clone, Deserialize, serde::Serialize)]
311#[serde(rename_all = "lowercase")]
312pub enum SearchType {
313    /// Search just by a match
314    NearMatch,
315    /// Search the content of the page
316    #[default]
317    Text,
318    /// Search the title of the page
319    Title,
320}
321
322impl Display for SearchType {
323    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324        match self {
325            SearchType::NearMatch => write!(f, "nearmatch"),
326            SearchType::Text => write!(f, "text"),
327            SearchType::Title => write!(f, "title"),
328        }
329    }
330}
331
332bitflags! {
333    /// A Search metadata
334    #[derive(Clone, Deserialize, serde::Serialize)]
335    pub struct Info: u8 {
336        /// The query if rewritten by the search backend. Refer to [`SearchBuilder::rewrites`] for more
337        /// information about rewrites by the search backend
338        ///
339        /// [`SearchBuilder::rewrites`]: SearchBuilder::rewrites
340        const REWRITTEN_QUERY = 0b00000001;
341        /// Another query to search instead for. This might include grammatical fixes
342        const SUGGESTION = 0b00000010;
343        /// The total amount of pages found for the query
344        const TOTAL_HITS = 0b00000100;
345    }
346}
347
348impl Default for Info {
349    fn default() -> Self {
350        Self::REWRITTEN_QUERY | Self::SUGGESTION | Self::TOTAL_HITS
351    }
352}
353
354impl fmt::Display for Info {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        let mut first = true;
357
358        if self.contains(Info::REWRITTEN_QUERY) {
359            if !first {
360                f.write_char('|')?;
361            }
362            first = false;
363
364            f.write_str("rewrittenquery")?;
365        }
366
367        if self.contains(Info::SUGGESTION) {
368            if !first {
369                f.write_char('|')?;
370            }
371            first = false;
372
373            f.write_str("suggestion")?;
374        }
375
376        if self.contains(Info::TOTAL_HITS) {
377            if !first {
378                f.write_char('|')?;
379            }
380
381            f.write_str("totalhits")?;
382        }
383
384        Ok(())
385    }
386}
387
388/// A Page property
389#[derive(serde::Serialize, serde::Deserialize)]
390pub enum Property {
391    /// The size of the page in bytes
392    Size,
393    /// The word count of the page
394    WordCount,
395    /// The timestamp wof when the page was last edited
396    Timestamp,
397    /// Snippet of the page, with query term highlighting markup
398    Snippet,
399    /// Page title, with query term highlighting markup
400    TitleSnippet,
401    /// Title of the matching redirect
402    RedirectTitle,
403    /// Title of the matching redirect, with query term highlighting markup
404    RedirectSnippet,
405    /// Title of the matching section
406    SectionTitle,
407    /// Title of the matching section, with query term highlighting markup
408    SectionSnippet,
409    /// Indicator whether the search matched file content
410    IsFileMatch,
411    /// Matching category name, with query term highlighting markup
412    CategorySnippet,
413}
414
415impl Display for Property {
416    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417        match self {
418            Property::Size => write!(f, "size"),
419            Property::WordCount => write!(f, "wordcount"),
420            Property::Timestamp => write!(f, "timestamp"),
421            Property::Snippet => write!(f, "snippet"),
422            Property::TitleSnippet => write!(f, "titlesnippet"),
423            Property::RedirectTitle => write!(f, "redirecttitle"),
424            Property::RedirectSnippet => write!(f, "redirectsnippet"),
425            Property::SectionTitle => write!(f, "sectiontitle"),
426            Property::SectionSnippet => write!(f, "sectionsnippet"),
427            Property::IsFileMatch => write!(f, "isfilematch"),
428            Property::CategorySnippet => write!(f, "categorysnippet"),
429        }
430    }
431}
432
433/// The sort order of returned search results
434#[derive(Deserialize, Clone, serde::Serialize)]
435#[serde(rename_all = "lowercase")]
436pub enum SortOrder {
437    /// Sort the results by their creation date in ascending order
438    CreateTimestampAscending,
439    /// Sort the results by their creation date in descending order
440    CreateTimestampDescending,
441    /// Sort the results by their amount of pages linking to it in ascending order
442    IncomingLinksAscending,
443    /// Sort the results by their amount of pages linking to it in descending order
444    IncomingLinksDescending,
445    /// Sort the results only by their match to the query
446    JustMatch,
447    /// Sort the results by the time of their last edit in ascending order
448    LastEditAscending,
449    /// Sort the results by the time of their last edit in descending order
450    LastEditDescending,
451    /// Do not sort the search results
452    NoSort,
453    /// Arrange the results in a random order
454    Random,
455    /// Sort the results by relevance
456    Relevance,
457    /// Arrange the results in a random order depending on the current user
458    UserRandom,
459}
460
461impl Display for SortOrder {
462    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
463        match self {
464            SortOrder::CreateTimestampAscending => write!(f, "create_timestamp_asc"),
465            SortOrder::CreateTimestampDescending => write!(f, "create_timestamp_desc"),
466            SortOrder::IncomingLinksAscending => write!(f, "incoming_links_asc"),
467            SortOrder::IncomingLinksDescending => write!(f, "incoming_links_desc"),
468            SortOrder::JustMatch => write!(f, "just_match"),
469            SortOrder::LastEditAscending => write!(f, "last_edit_asc"),
470            SortOrder::LastEditDescending => write!(f, "last_edit_desc"),
471            SortOrder::NoSort => write!(f, "none"),
472            SortOrder::Random => write!(f, "random"),
473            SortOrder::Relevance => write!(f, "relevance"),
474            SortOrder::UserRandom => write!(f, "user_random"),
475        }
476    }
477}
478
479#[doc(hidden)]
480pub struct WithQuery(String);
481
482#[doc(hidden)]
483#[derive(Default)]
484pub struct NoQuery;
485
486#[doc(hidden)]
487pub struct WithEndpoint(Endpoint);
488
489#[doc(hidden)]
490#[derive(Default)]
491pub struct NoEndpoint;
492
493#[doc(hidden)]
494pub struct WithLanguage(Language);
495
496#[doc(hidden)]
497#[derive(Default)]
498pub struct NoLanguage;
499
500/// A fully configured `SearchBuilder` that can be used to execute the search. This is a convenience type
501pub type SearchRequest = SearchBuilder<WithQuery, WithEndpoint, WithLanguage>;
502
503/// A `SearchBuilder` can be used to configure and perform a search
504#[derive(Default)]
505pub struct SearchBuilder<Q, E, L> {
506    query: Q,
507    endpoint: E,
508    language: L,
509    namespace: Option<Namespace>,
510    limit: Option<usize>,
511    offset: Option<usize>,
512    qiprofile: Option<QiProfile>,
513    search_type: Option<SearchType>,
514    info: Option<Info>,
515    properties: Option<Vec<Property>>,
516    interwiki: Option<bool>,
517    rewrites: Option<bool>,
518    sort_order: Option<SortOrder>,
519}
520
521impl<E, L> SearchBuilder<NoQuery, E, L> {
522    /// Search for page titles or content matching this value
523    pub fn query(self, query: impl Into<String>) -> SearchBuilder<WithQuery, E, L> {
524        SearchBuilder {
525            query: WithQuery(query.into()),
526            endpoint: self.endpoint,
527            language: self.language,
528            namespace: self.namespace,
529            limit: self.limit,
530            offset: self.offset,
531            qiprofile: self.qiprofile,
532            search_type: self.search_type,
533            info: self.info,
534            properties: self.properties,
535            interwiki: self.interwiki,
536            rewrites: self.rewrites,
537            sort_order: self.sort_order,
538        }
539    }
540}
541
542impl<Q, L> SearchBuilder<Q, NoEndpoint, L> {
543    /// API endpoint for the MediaWiki site to perform the search on
544    pub fn endpoint(self, endpoint: Endpoint) -> SearchBuilder<Q, WithEndpoint, L> {
545        SearchBuilder {
546            query: self.query,
547            endpoint: WithEndpoint(endpoint),
548            language: self.language,
549            namespace: self.namespace,
550            limit: self.limit,
551            offset: self.offset,
552            qiprofile: self.qiprofile,
553            search_type: self.search_type,
554            info: self.info,
555            properties: self.properties,
556            interwiki: self.interwiki,
557            rewrites: self.rewrites,
558            sort_order: self.sort_order,
559        }
560    }
561}
562
563impl<Q, E> SearchBuilder<Q, E, NoLanguage> {
564    /// Language where the search will be performed in
565    pub fn language(self, language: Language) -> SearchBuilder<Q, E, WithLanguage> {
566        SearchBuilder {
567            query: self.query,
568            endpoint: self.endpoint,
569            language: WithLanguage(language),
570            namespace: self.namespace,
571            limit: self.limit,
572            offset: self.offset,
573            qiprofile: self.qiprofile,
574            search_type: self.search_type,
575            info: self.info,
576            properties: self.properties,
577            interwiki: self.interwiki,
578            rewrites: self.rewrites,
579            sort_order: self.sort_order,
580        }
581    }
582}
583
584impl<Q, E, L> SearchBuilder<Q, E, L> {
585    /// Search only in this specific namespace
586    pub fn namespace(mut self, namespace: Namespace) -> Self {
587        self.namespace = Some(namespace);
588        self
589    }
590
591    /// How many total pages to return. The value must be between 1 and 500
592    ///
593    /// Default: `10`
594    pub fn limit(mut self, limit: usize) -> Self {
595        self.limit = Some(limit);
596        self
597    }
598
599    /// When more results are available, use the offset to continue
600    /// Default: `0`
601    pub fn offset(mut self, offset: usize) -> Self {
602        self.offset = Some(offset);
603        self
604    }
605
606    /// Query independent profile to use which affects the ranking algorithm
607    ///
608    /// Default: [`QiProfile::EngineAutoselect`]
609    ///
610    /// [`QiProfile::EngineAutoselect`]: QiProfile::EngineAutoselect
611    pub fn qiprofile(mut self, qiprofile: QiProfile) -> Self {
612        self.qiprofile = Some(qiprofile);
613        self
614    }
615
616    /// Which search to perform
617    ///
618    /// Default: [`SearchType::Text`]
619    ///
620    /// [`SearchType::Text`]: SearchType::Text
621    pub fn search_type(mut self, search_type: SearchType) -> Self {
622        self.search_type = Some(search_type);
623        self
624    }
625
626    /// Which metadata to return
627    ///
628    /// Default: [[`Info::TotalHits`], [`Info::Suggestion`], [`Info::RewrittenQuery`]]
629    ///
630    /// [`Info::TotalHits`]: Info::TotalHits
631    /// [`Info::Suggestion`]: Info::Suggestion
632    /// [`Info::RewrittenQuery`]: Info::RewrittenQuery
633    pub fn info(mut self, info: Info) -> Self {
634        self.info = Some(info);
635        self
636    }
637
638    /// Which properties about the search results to return
639    ///
640    /// Default: [[`Property::Size`], [`Property::WordCount`], [`Property::Timestamp`],
641    /// [`Property::Snippet`]]
642    ///
643    /// [`Property::Size`]: Property::Size
644    /// [`Property::WordCount`]: Property::WordCount
645    /// [`Property::Timestamp`]: Property::Timestamp
646    /// [`Property::Snippet`]: Property::Snippet
647    pub fn properties(mut self, properties: Vec<Property>) -> Self {
648        self.properties = Some(properties);
649        self
650    }
651
652    /// Include interwiki results in the search, if available
653    ///
654    /// Default: `false`
655    pub fn interwiki(mut self, interwiki: bool) -> Self {
656        self.interwiki = Some(interwiki);
657        self
658    }
659
660    /// Enable internal query rewriting. Some search backends can rewrite the query into another
661    /// which is thought to provide better results, for instance by correcting spelling errors
662    ///
663    /// Default: `false`
664    pub fn rewrites(mut self, rewrites: bool) -> Self {
665        self.rewrites = Some(rewrites);
666        self
667    }
668
669    /// Set the sort order of returend results
670    ///
671    /// Default: [`SortOrder::Relevance`]
672    ///
673    /// [`SortOrder::Relevance`]: SortOrder::Relevance
674    pub fn sort_order(mut self, sort_order: SortOrder) -> Self {
675        self.sort_order = Some(sort_order);
676        self
677    }
678}
679
680impl SearchBuilder<WithQuery, WithEndpoint, WithLanguage> {
681    /// Performes the search and returns the result. The search can only be made when the query,
682    /// endpoint and language are set
683    ///
684    /// # Example
685    ///
686    /// ```
687    /// // This searches for the pages containing 'meaning' in the english wikipedia
688    /// let search = Search::builder()
689    ///     .query("meaning")
690    ///     .endpoint(Url::from("https://en.wikipedia.org/w/api.php")?)
691    ///     .language(Language::English)
692    ///     .search()?;
693    /// ```
694    ///
695    /// # Error
696    ///
697    /// This function returns an error when one of the following things happens:
698    /// - The request to the server could not be made
699    /// - The server returned an error
700    /// - The returned result could not interpreted as a `Search`
701    pub async fn search(self) -> Result<Search> {
702        async fn action_query(params: Vec<(&str, String)>, endpoint: Endpoint) -> Result<Response> {
703            Client::new()
704                .get(endpoint)
705                .header(
706                    "User-Agent",
707                    format!(
708                        "wiki-tui/{} (https://github.com/Builditluc/wiki-tui)",
709                        env!("CARGO_PKG_VERSION")
710                    ),
711                )
712                .query(&[
713                    ("action", "query"),
714                    ("format", "json"),
715                    ("formatversion", "2"),
716                ])
717                .query(&params)
718                .send()
719                .await
720                .context("failed sending the request")
721        }
722
723        let mut params = vec![
724            ("list", "search".to_string()),
725            ("srsearch", self.query.0.clone()),
726        ];
727
728        if let Some(namespace) = self.namespace {
729            params.push(("srnamespace", namespace.to_string()));
730        }
731
732        if let Some(limit) = self.limit {
733            params.push(("srlimit", limit.to_string()));
734        }
735
736        if let Some(offset) = self.offset {
737            params.push(("sroffset", offset.to_string()));
738        }
739
740        if let Some(qiprofile) = self.qiprofile {
741            params.push(("srqiprofile", qiprofile.to_string()));
742        }
743
744        if let Some(search_type) = self.search_type {
745            params.push(("srwhat", search_type.to_string()));
746        }
747
748        if let Some(info) = self.info {
749            params.push(("srinfo", info.to_string()));
750        }
751
752        if let Some(prop) = self.properties {
753            let mut prop_str = String::new();
754            for prop in prop {
755                prop_str.push('|');
756                prop_str.push_str(&prop.to_string());
757            }
758            params.push(("srprop", prop_str));
759        }
760
761        if let Some(interwiki) = self.interwiki {
762            params.push(("srinterwiki", interwiki.to_string()));
763        }
764
765        if let Some(rewrites) = self.rewrites {
766            params.push(("srenablerewrites", rewrites.to_string()));
767        }
768
769        if let Some(sort_order) = self.sort_order {
770            params.push(("srsort", sort_order.to_string()));
771        }
772
773        let response = action_query(params, self.endpoint.0.clone())
774            .await?
775            .error_for_status()
776            .context("the server returned an error")?;
777
778        let res_json: serde_json::Value = serde_json::from_str(
779            &response
780                .text()
781                .await
782                .context("failed reading the response")?,
783        )
784        .context("failed interpreting the response as json")?;
785
786        let continue_offset = res_json
787            .get("continue")
788            .and_then(|x| x.get("sroffset"))
789            .and_then(|x| x.as_u64().map(|x| x as usize));
790
791        let total_hits = res_json
792            .get("query")
793            .and_then(|x| x.get("searchinfo"))
794            .and_then(|x| x.get("totalhits"))
795            .and_then(|x| x.as_u64().map(|x| x as usize));
796
797        let suggestion = res_json
798            .get("query")
799            .and_then(|x| x.get("searchinfo"))
800            .and_then(|x| x.get("suggestion"))
801            .and_then(|x| x.as_str().map(|x| x.to_string()));
802
803        let rewritten_query = res_json
804            .get("query")
805            .and_then(|x| x.get("searchinfo"))
806            .and_then(|x| x.get("rewrittenquery"))
807            .and_then(|x| x.as_str().map(|x| x.to_string()));
808
809        let results: Vec<SearchResult> = {
810            let mut results: Vec<SearchResult> = Vec::new();
811            let results_json = res_json
812                .get("query")
813                .and_then(|x| x.get("search"))
814                .and_then(|x| x.as_array())
815                .ok_or_else(|| anyhow!("missing the search results"))?
816                .to_owned();
817
818            macro_rules! value_from_json {
819                ($result: ident, $val: expr) => {
820                    serde_json::from_value($result.get($val).map(|x| x.to_owned()).ok_or_else(
821                        || {
822                            anyhow!(
823                                "couldn't find '{}'
824 in the result",
825                                stringify!($val)
826                            )
827                        },
828                    )?)?
829                };
830            }
831
832            for result in results_json.into_iter() {
833                results.push(SearchResult {
834                    namespace: value_from_json!(result, "ns"),
835                    title: value_from_json!(result, "title"),
836                    pageid: value_from_json!(result, "pageid"),
837                    language: self.language.0,
838                    endpoint: self.endpoint.0.clone(),
839                    size: value_from_json!(result, "size"),
840                    wordcount: value_from_json!(result, "wordcount"),
841                    snippet: value_from_json!(result, "snippet"),
842                    timestamp: value_from_json!(result, "timestamp"),
843                })
844            }
845
846            results
847        };
848
849        let info = SearchInfo {
850            complete: continue_offset.is_none(),
851            total_hits,
852            suggestion,
853            rewritten_query,
854            query: self.query.0,
855            language: self.language.0,
856        };
857
858        Ok(Search {
859            continue_offset,
860            results,
861            endpoint: self.endpoint.0,
862            info,
863        })
864    }
865}