datamuse_api_wrapper/
request.rs

1use crate::response::{Response, WordElement};
2use crate::{DatamuseClient, Error, Result};
3use reqwest;
4use std::fmt::{self, Display, Formatter};
5
6/// Use this struct to build requests to send to the Datamuse api.
7/// This request can be sent either by building it into a Request with build()
8/// and then using the send() method on the resulting Request or using send() to
9/// send it directly. Note that not all parameters can be used for each vocabulary
10/// and endpoint
11#[derive(Debug)]
12pub struct RequestBuilder<'a> {
13    client: &'a DatamuseClient,
14    endpoint: EndPoint,
15    vocabulary: Vocabulary,
16    parameters: Vec<Parameter>,
17    topics: Vec<String>, //Makes adding topics make easier, later added to parameters
18    meta_data_flags: Vec<MetaDataFlag>, //Same issue as topics
19}
20
21/// This struct represents a built request that can be sent using the send() method
22#[derive(Debug)]
23pub struct Request<'a> {
24    client: &'a reqwest::Client,
25    request: reqwest::Request,
26}
27
28/// This enum represents the different endpoints of the Datamuse api.
29/// The "words" endpoint returns word lists based on a set of parameters,
30/// whereas the "suggest" endpoint returns suggestions for words based on a
31/// hint string (autocomplete).
32/// For more detailed information visit the [Datamuse website](https://www.datamuse.com/api/)
33#[derive(Clone, Copy, Debug)]
34pub enum EndPoint {
35    /// The "words" endpoint (the official endpoint is also "/words")
36    Words,
37    /// The "suggest" endpoint (the official endpoint is "/sug")
38    Suggest,
39}
40
41/// This enum represents the different vocabulary lists which can be used as
42/// a source for the requests. There are currently two language options
43/// (English or Spanish) and an alternative English option from wikipedia.
44/// For more detailed information visit the [Datamuse website](https://www.datamuse.com/api/)
45#[derive(Clone, Copy, Debug)]
46pub enum Vocabulary {
47    /// The default vocabulary list with 550,000 words
48    English,
49    /// The Spanish vocabulary list with 500,000 words
50    Spanish,
51    /// The alternative English vocabulary list with 6 million words
52    EnglishWiki,
53}
54
55/// This enum represents the different possibilites the "Related" parameter can take.
56/// These parameters can be combined in any possible configuration, although very specific
57/// queries can limit results. Each option is shortly explained below.
58/// For more detailed information for each type visit the [Datamuse website](https://www.datamuse.com/api/)
59#[derive(Clone, Copy, Debug)]
60pub enum RelatedType {
61    /// This parameter returns nouns that are typically modified by the given adjective
62    NounModifiedBy,
63    /// This parameter returns adjectives that typically modify by the given noun
64    AdjectiveModifier,
65    /// This parameter returns synonyms for the given word
66    Synonym,
67    /// This parameter returns associated words for the given word
68    Trigger,
69    /// This parameter returns antonyms for the given word
70    Antonym,
71    /// This parameter returns the kind of which a more specific word is
72    KindOf,
73    /// This parameter returns a more specific kind of the given category word (opposite of KindOf)
74    MoreGeneral,
75    /// This parameter returns words that describe things which the given word is comprised of
76    Comprises,
77    /// This parameter returns words that describe things which the given word is a part of (opposite of Comprises)
78    PartOf,
79    /// This parameter returns words that are typically found after the given word
80    Follower,
81    /// This parameter returns words that are typically found before the given word
82    Predecessor,
83    /// This parameter returns words that rhyme with the given word
84    Rhyme,
85    /// This parameter returns words that almost rhyme with the given word
86    ApproximateRhyme,
87    /// This parameter returns words that sound like the given word
88    Homophones,
89    /// This parameter returns words which have matching consonants but differing vowels from the given word
90    ConsonantMatch,
91}
92
93/// This enum represents the various flags which can be set for retrieving metadata for each word.
94/// These metadata flags can be combined in any manner. Each is shortly described below
95#[derive(Clone, Copy, Debug)]
96pub enum MetaDataFlag {
97    /// Provides definitions for each of the words in the response
98    Definitions,
99    /// Provides what type of speech each of the words in the response is
100    PartsOfSpeech,
101    /// Provides how many syllables each of the words in the response has
102    SyllableCount,
103    /// Provides pronunciations for each of the words in the response based on
104    /// the given pronunciation format
105    Pronunciation(PronunciationFormat),
106    /// Provides how frequently each of the words in the response is found
107    WordFrequency,
108}
109
110/// This enum represents the ways pronunciations returned by the "Pronunciation" metadata flag
111/// can be given
112#[derive(Clone, Copy, Debug)]
113pub enum PronunciationFormat {
114    /// The [ARPABET](https://en.wikipedia.org/wiki/ARPABET) pronunciation format
115    Arpabet,
116    /// The [International Phonetic Alphabet](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet) pronunciation format
117    Ipa,
118}
119
120#[derive(Clone, Debug)]
121struct RelatedTypeHolder {
122    related_type: RelatedType,
123    value: String,
124}
125
126#[derive(Clone, Debug)]
127enum Parameter {
128    MeansLike(String),
129    SoundsLike(String),
130    SpelledLike(String),
131    Related(RelatedTypeHolder),
132    Topics(Vec<String>),
133    LeftContext(String),
134    RightContext(String),
135    MaxResults(u16), //Also supported for sug endpoint
136    MetaData(Vec<MetaDataFlag>),
137    HintString(String), //Only supported for sug endpoint
138}
139
140impl<'a> RequestBuilder<'a> {
141    /// Sets a query parameter for words which have a similar meaning to the given word
142    pub fn means_like(mut self, word: &str) -> Self {
143        self.parameters
144            .push(Parameter::MeansLike(String::from(word)));
145
146        self
147    }
148
149    /// Sets a query parameter for words which sound similar to the given word
150    pub fn sounds_like(mut self, word: &str) -> Self {
151        self.parameters
152            .push(Parameter::SoundsLike(String::from(word)));
153
154        self
155    }
156
157    /// Sets a query parameter for words which have a similar spelling to the given word.
158    /// This parameter allows for wildcard charcters with '?' matching a single letter and
159    /// '*' matching any number of letters
160    pub fn spelled_like(mut self, word: &str) -> Self {
161        self.parameters
162            .push(Parameter::SpelledLike(String::from(word)));
163
164        self
165    }
166
167    /// Sets a query parameter for words which are related to the given word.
168    /// The various options for relations are given in the [RelatedType](RelatedType) enum.
169    /// See its documentation for more information on the options.
170    /// Note that this is currently **not available** for the Spanish vocabulary set
171    pub fn related(mut self, rel_type: RelatedType, word: &str) -> Self {
172        self.parameters.push(Parameter::Related(RelatedTypeHolder {
173            related_type: rel_type,
174            value: String::from(word),
175        }));
176
177        self
178    }
179
180    /// Sets a query parameter for words which fall under the topic of the given word.
181    /// Multiple topics can be specified at once, however requests are limited to five
182    /// topics and as such any specified over this limit will be ignored
183    pub fn add_topic(mut self, word: &str) -> Self {
184        self.topics.push(String::from(word));
185
186        self
187    }
188
189    /// Sets a query parameter to refer to the word directly before the main query term
190    pub fn left_context(mut self, word: &str) -> Self {
191        self.parameters
192            .push(Parameter::LeftContext(String::from(word)));
193
194        self
195    }
196
197    /// Sets a query parameter to refer to the word directly after the main query term
198    pub fn right_context(mut self, word: &str) -> Self {
199        self.parameters
200            .push(Parameter::RightContext(String::from(word)));
201
202        self
203    }
204
205    /// The maximum number of results that should be returned. By default this is set to 100
206    /// and it can be increased to a maximum of 1000. This parameter is also **allowed** for the
207    /// "suggest" endpoint
208    pub fn max_results(mut self, maximum: u16) -> Self {
209        self.parameters.push(Parameter::MaxResults(maximum));
210
211        self
212    }
213
214    /// Sets a metadata flag to specify data returned with each word.
215    /// The various options for flags are given in the [MetaDataFlag](MetaDataFlag) enum.
216    /// See its documentation for more information on the options
217    pub fn meta_data(mut self, flag: MetaDataFlag) -> Self {
218        self.meta_data_flags.push(flag);
219
220        self
221    }
222
223    /// Sets the hint string for the "suggest" endpoint. Note that this is
224    /// **not allowed** for the "words" endpoint
225    pub fn hint_string(mut self, hint: &str) -> Self {
226        self.parameters
227            .push(Parameter::HintString(String::from(hint)));
228
229        self
230    }
231
232    /// Converts the RequestBuilder into a Request which can be executed by calling the send()
233    /// method on it. This method will return an error if any of the given parameters have not been
234    /// used correctly or the underlying call to reqwest to build the request fails
235    pub fn build(&self) -> Result<Request> {
236        let mut params_list: Vec<(String, String)> = Vec::new();
237        let mut parameters = self.parameters.clone();
238
239        if self.topics.len() > 0 {
240            parameters.push(Parameter::Topics(self.topics.clone()));
241        }
242
243        if self.meta_data_flags.len() > 0 {
244            parameters.push(Parameter::MetaData(self.meta_data_flags.clone()));
245
246            for flag in self.meta_data_flags.clone() {
247                if let MetaDataFlag::Pronunciation(PronunciationFormat::Ipa) = flag {
248                    params_list.push((String::from("ipa"), 1.to_string()));
249                }
250            }
251        }
252
253        let vocab_params = self.vocabulary.build();
254        if let Some(val) = vocab_params {
255            params_list.push(val);
256        }
257
258        for param in parameters {
259            params_list.push(param.build(&self.vocabulary, &self.endpoint)?);
260        }
261
262        let request = self
263            .client
264            .client
265            .get(&format!(
266                "https://api.datamuse.com/{}",
267                self.endpoint.get_string()
268            ))
269            .query(&params_list)
270            .build()?;
271
272        Ok(Request {
273            request,
274            client: &self.client.client,
275        })
276    }
277
278    /// A convenience method to build and send the request in one step. The resulting
279    /// response can be parsed with its list() method
280    pub async fn send(&self) -> Result<Response> {
281        self.build()?.send().await
282    }
283
284    /// A convenience method to build and send the request as well as parse the json in one step
285    pub async fn list(&self) -> Result<Vec<WordElement>> {
286        self.send().await?.list()
287    }
288
289    pub(crate) fn new(
290        client: &'a DatamuseClient,
291        vocabulary: Vocabulary,
292        endpoint: EndPoint,
293    ) -> Self {
294        RequestBuilder {
295            client,
296            endpoint,
297            vocabulary,
298            parameters: Vec::new(),
299            topics: Vec::new(),
300            meta_data_flags: Vec::new(),
301        }
302    }
303}
304
305impl<'a> Request<'a> {
306    /// Sends the built request and returns the response. This response can later be parsed with its
307    /// list() method
308    pub async fn send(self) -> Result<Response> {
309        let json = self.client.execute(self.request).await?.text().await?;
310        Ok(Response::new(json))
311    }
312}
313
314impl Parameter {
315    fn build(&self, vocab: &Vocabulary, endpoint: &EndPoint) -> Result<(String, String)> {
316        if let Parameter::Related(_) = self {
317            //Error for using related with spanish vocabulary
318            if let Vocabulary::Spanish = vocab {
319                return Err(Error::VocabularyError((
320                    String::from("Spanish"),
321                    String::from("Related"),
322                )));
323            }
324        }
325
326        if let EndPoint::Words = endpoint {
327            //Error for using hint string for the words endpoint
328            if let Parameter::HintString(_) = self {
329                return Err(Error::EndPointError((
330                    String::from("Words"),
331                    String::from("HintString"),
332                )));
333            }
334        }
335
336        if let EndPoint::Suggest = endpoint {
337            match self {
338                Parameter::MaxResults(_) => (),
339                Parameter::HintString(_) => (),
340                val => {
341                    return Err(Error::EndPointError((
342                        String::from("Suggest"),
343                        format!("{}", val),
344                    )));
345                }
346            }
347        }
348
349        let param = match self {
350            Self::MeansLike(val) => (String::from("ml"), val.clone()),
351            Self::SoundsLike(val) => (String::from("sl"), val.clone()),
352            Self::SpelledLike(val) => (String::from("sp"), val.clone()),
353            Self::Related(val) => (format!("rel_{}", val.get_type_identifier()), val.get_word()),
354            Self::Topics(topic_list) => {
355                let mut topics_concat = String::from("");
356                let mut len = topic_list.len();
357
358                if len > 5 {
359                    len = 5;
360                }
361
362                let mut i = 0;
363                while i < len - 1 {
364                    topics_concat = topics_concat + &topic_list[i];
365                    topics_concat.push(',');
366                    i += 1;
367                }
368                topics_concat = topics_concat + &topic_list[len - 1];
369
370                (String::from("topics"), topics_concat)
371            }
372            Self::LeftContext(val) => (String::from("lc"), val.clone()),
373            Self::RightContext(val) => (String::from("rc"), val.clone()),
374            Self::MaxResults(val) => (String::from("max"), val.to_string()),
375            Self::MetaData(flags) => {
376                let mut flags_concat = String::from("");
377                for flag in flags {
378                    flags_concat.push(flag.get_letter_identifier());
379                }
380
381                (String::from("md"), flags_concat)
382            }
383            Self::HintString(val) => (String::from("s"), val.clone()),
384        };
385
386        Ok(param)
387    }
388}
389
390impl Display for Parameter {
391    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
392        let name = match self {
393            Self::MeansLike(_) => "MeansLike",
394            Self::SoundsLike(_) => "SoundsLike",
395            Self::SpelledLike(_) => "SpelledLike",
396            Self::Related(_) => "Related",
397            Self::Topics(_) => "Topic",
398            Self::LeftContext(_) => "LeftContext",
399            Self::RightContext(_) => "RightContext",
400            Self::MaxResults(_) => "MaxResults",
401            Self::MetaData(_) => "MetaData",
402            Self::HintString(_) => "HintString",
403        };
404
405        write!(f, "{}", name)
406    }
407}
408
409impl RelatedTypeHolder {
410    fn get_type_identifier(&self) -> String {
411        match self.related_type {
412            RelatedType::NounModifiedBy => String::from("jja"),
413            RelatedType::AdjectiveModifier => String::from("jjb"),
414            RelatedType::Synonym => String::from("syn"),
415            RelatedType::Trigger => String::from("trg"),
416            RelatedType::Antonym => String::from("ant"),
417            RelatedType::KindOf => String::from("spc"),
418            RelatedType::MoreGeneral => String::from("gen"),
419            RelatedType::Comprises => String::from("com"),
420            RelatedType::PartOf => String::from("par"),
421            RelatedType::Follower => String::from("bga"),
422            RelatedType::Predecessor => String::from("bgb"),
423            RelatedType::Rhyme => String::from("rhy"),
424            RelatedType::ApproximateRhyme => String::from("nry"),
425            RelatedType::Homophones => String::from("hom"),
426            RelatedType::ConsonantMatch => String::from("cns"),
427        }
428    }
429
430    fn get_word(&self) -> String {
431        self.value.clone()
432    }
433}
434
435impl MetaDataFlag {
436    fn get_letter_identifier(&self) -> char {
437        match self {
438            Self::Definitions => 'd',
439            Self::PartsOfSpeech => 'p',
440            Self::SyllableCount => 's',
441            Self::Pronunciation(_) => 'r',
442            Self::WordFrequency => 'f',
443        }
444    }
445}
446
447impl EndPoint {
448    fn get_string(&self) -> String {
449        match self {
450            Self::Words => String::from("words"),
451            Self::Suggest => String::from("sug"),
452        }
453    }
454}
455
456impl Vocabulary {
457    fn build(&self) -> Option<(String, String)> {
458        match self {
459            Vocabulary::Spanish => Some((String::from("v"), String::from("es"))),
460            Vocabulary::EnglishWiki => Some((String::from("v"), String::from("enwiki"))),
461            Vocabulary::English => None,
462        }
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use crate::{
469        DatamuseClient, EndPoint, MetaDataFlag, PronunciationFormat, RelatedType, Vocabulary,
470    };
471
472    #[test]
473    fn means_like_and_sounds_like() {
474        let client = DatamuseClient::new();
475        let request = client
476            .new_query(Vocabulary::English, EndPoint::Words)
477            .means_like("cap")
478            .sounds_like("flat");
479
480        assert_eq!(
481            "https://api.datamuse.com/words?ml=cap&sl=flat",
482            request.build().unwrap().request.url().as_str()
483        );
484    }
485
486    #[test]
487    fn left_context_and_spelled_like() {
488        let client = DatamuseClient::new();
489        let request = client
490            .new_query(Vocabulary::English, EndPoint::Words)
491            .left_context("drink")
492            .spelled_like("w*");
493
494        assert_eq!(
495            "https://api.datamuse.com/words?lc=drink&sp=w*",
496            request.build().unwrap().request.url().as_str()
497        );
498    }
499
500    #[test]
501    fn right_context_and_max_results() {
502        let client = DatamuseClient::new();
503        let request = client
504            .new_query(Vocabulary::English, EndPoint::Words)
505            .right_context("food")
506            .max_results(500);
507
508        assert_eq!(
509            "https://api.datamuse.com/words?rc=food&max=500",
510            request.build().unwrap().request.url().as_str()
511        );
512    }
513
514    #[test]
515    fn topics_and_sounds_like() {
516        let client = DatamuseClient::new();
517        let request = client
518            .new_query(Vocabulary::English, EndPoint::Words)
519            .add_topic("color")
520            .sounds_like("clue")
521            .add_topic("sad");
522
523        assert_eq!(
524            "https://api.datamuse.com/words?sl=clue&topics=color%2Csad", //%2C = ','
525            request.build().unwrap().request.url().as_str()
526        );
527    }
528
529    #[test]
530    fn suggest_endpoint() {
531        let client = DatamuseClient::new();
532        let request = client
533            .new_query(Vocabulary::English, EndPoint::Suggest)
534            .hint_string("hel")
535            .max_results(20);
536
537        assert_eq!(
538            "https://api.datamuse.com/sug?s=hel&max=20",
539            request.build().unwrap().request.url().as_str()
540        );
541    }
542
543    #[test]
544    #[should_panic]
545    fn suggest_endpoint_fail() {
546        let client = DatamuseClient::new();
547        let request = client
548            .new_query(Vocabulary::English, EndPoint::Suggest)
549            .add_topic("color");
550        request.build().unwrap();
551    }
552
553    #[test]
554    #[should_panic]
555    fn words_endpoint_fail() {
556        let client = DatamuseClient::new();
557        let request = client
558            .new_query(Vocabulary::English, EndPoint::Words)
559            .add_topic("color")
560            .hint_string("blu");
561        request.build().unwrap();
562    }
563
564    #[test]
565    #[should_panic]
566    fn spanish_vocabulary_fail() {
567        let client = DatamuseClient::new();
568        let request = client
569            .new_query(Vocabulary::Spanish, EndPoint::Words)
570            .related(RelatedType::Trigger, "frutas")
571            .sounds_like("manta");
572
573        request.build().unwrap();
574    }
575
576    #[test]
577    fn noun_and_adjective_modifiers() {
578        let client = DatamuseClient::new();
579        let request = client
580            .new_query(Vocabulary::English, EndPoint::Words)
581            .related(RelatedType::AdjectiveModifier, "food")
582            .related(RelatedType::NounModifiedBy, "fresh");
583
584        assert_eq!(
585            "https://api.datamuse.com/words?rel_jjb=food&rel_jja=fresh",
586            request.build().unwrap().request.url().as_str()
587        );
588    }
589
590    #[test]
591    fn synonyms_and_triggers() {
592        let client = DatamuseClient::new();
593        let request = client
594            .new_query(Vocabulary::English, EndPoint::Words)
595            .related(RelatedType::Synonym, "grass")
596            .related(RelatedType::Trigger, "cow");
597
598        assert_eq!(
599            "https://api.datamuse.com/words?rel_syn=grass&rel_trg=cow",
600            request.build().unwrap().request.url().as_str()
601        );
602    }
603
604    #[test]
605    fn antonyms_and_consonant_match() {
606        let client = DatamuseClient::new();
607        let request = client
608            .new_query(Vocabulary::English, EndPoint::Words)
609            .related(RelatedType::Antonym, "good")
610            .related(RelatedType::ConsonantMatch, "bed");
611
612        assert_eq!(
613            "https://api.datamuse.com/words?rel_ant=good&rel_cns=bed",
614            request.build().unwrap().request.url().as_str()
615        );
616    }
617
618    #[test]
619    fn kind_of_and_more_general() {
620        let client = DatamuseClient::new();
621        let request = client
622            .new_query(Vocabulary::English, EndPoint::Words)
623            .related(RelatedType::KindOf, "wagon")
624            .related(RelatedType::MoreGeneral, "vehicle");
625
626        assert_eq!(
627            "https://api.datamuse.com/words?rel_spc=wagon&rel_gen=vehicle",
628            request.build().unwrap().request.url().as_str()
629        );
630    }
631
632    #[test]
633    fn comprises_and_part_of() {
634        let client = DatamuseClient::new();
635        let request = client
636            .new_query(Vocabulary::English, EndPoint::Words)
637            .related(RelatedType::Comprises, "car")
638            .related(RelatedType::PartOf, "glass");
639
640        assert_eq!(
641            "https://api.datamuse.com/words?rel_com=car&rel_par=glass",
642            request.build().unwrap().request.url().as_str()
643        );
644    }
645
646    #[test]
647    fn follows_and_precedes() {
648        let client = DatamuseClient::new();
649        let request = client
650            .new_query(Vocabulary::English, EndPoint::Words)
651            .related(RelatedType::Follower, "soda")
652            .related(RelatedType::Predecessor, "drink");
653
654        assert_eq!(
655            "https://api.datamuse.com/words?rel_bga=soda&rel_bgb=drink",
656            request.build().unwrap().request.url().as_str()
657        );
658    }
659
660    #[test]
661    fn both_rhymes_and_homophones() {
662        let client = DatamuseClient::new();
663        let request = client
664            .new_query(Vocabulary::English, EndPoint::Words)
665            .related(RelatedType::Rhyme, "cat")
666            .related(RelatedType::Homophones, "mate")
667            .related(RelatedType::ApproximateRhyme, "fate");
668
669        assert_eq!(
670            "https://api.datamuse.com/words?rel_rhy=cat&rel_hom=mate&rel_nry=fate",
671            request.build().unwrap().request.url().as_str()
672        );
673    }
674
675    #[test]
676    fn all_meta_data_flags() {
677        let client = DatamuseClient::new();
678        let request = client
679            .new_query(Vocabulary::English, EndPoint::Words)
680            .related(RelatedType::Trigger, "cow")
681            .meta_data(MetaDataFlag::Definitions)
682            .meta_data(MetaDataFlag::PartsOfSpeech)
683            .meta_data(MetaDataFlag::SyllableCount)
684            .meta_data(MetaDataFlag::WordFrequency)
685            .meta_data(MetaDataFlag::Pronunciation(PronunciationFormat::Arpabet));
686
687        assert_eq!(
688            "https://api.datamuse.com/words?rel_trg=cow&md=dpsfr",
689            request.build().unwrap().request.url().as_str()
690        );
691    }
692
693    #[test]
694    fn pronunciation_ipa() {
695        let client = DatamuseClient::new();
696        let request = client
697            .new_query(Vocabulary::English, EndPoint::Words)
698            .related(RelatedType::Trigger, "soda")
699            .meta_data(MetaDataFlag::Pronunciation(PronunciationFormat::Ipa));
700
701        assert_eq!(
702            "https://api.datamuse.com/words?ipa=1&rel_trg=soda&md=r",
703            request.build().unwrap().request.url().as_str()
704        );
705    }
706}