datamuse_api_rs/
request.rs

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