1use crate::response::{Response, WordElement};
2use crate::{DatamuseClient, Error, Result};
3use std::fmt::{self, Display, Formatter};
4
5#[derive(Debug)]
11pub struct RequestBuilder<'a> {
12 client: &'a DatamuseClient,
13 endpoint: EndPoint,
14 vocabulary: Vocabulary,
15 parameters: Vec<Parameter>,
16 topics: Vec<String>, meta_data_flags: Vec<MetaDataFlag>, }
19
20#[derive(Debug)]
22pub struct Request<'a> {
23 client: &'a reqwest::Client,
24 request: reqwest::Request,
25}
26
27#[derive(Clone, Copy, Debug)]
33pub enum EndPoint {
34 Words,
36 Suggest,
38}
39
40#[derive(Clone, Copy, Debug)]
45pub enum Vocabulary {
46 English,
48 Spanish,
50 EnglishWiki,
52}
53
54#[derive(Clone, Copy, Debug)]
59pub enum RelatedType {
60 NounModifiedBy,
62 AdjectiveModifier,
64 Synonym,
66 Trigger,
68 Antonym,
70 KindOf,
72 MoreGeneral,
74 Comprises,
76 PartOf,
78 Follower,
80 Predecessor,
82 Rhyme,
84 ApproximateRhyme,
86 Homophones,
88 ConsonantMatch,
90}
91
92#[derive(Clone, Copy, Debug)]
95pub enum MetaDataFlag {
96 Definitions,
98 PartsOfSpeech,
100 SyllableCount,
102 Pronunciation(PronunciationFormat),
105 WordFrequency,
107}
108
109#[derive(Clone, Copy, Debug)]
112pub enum PronunciationFormat {
113 Arpabet,
115 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), MetaData(Vec<MetaDataFlag>),
136 HintString(String), }
138
139impl<'a> RequestBuilder<'a> {
140 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 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 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 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 pub fn add_topic(mut self, word: &str) -> Self {
183 self.topics.push(String::from(word));
184
185 self
186 }
187
188 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 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 pub fn max_results(mut self, maximum: u16) -> Self {
208 self.parameters.push(Parameter::MaxResults(maximum));
209
210 self
211 }
212
213 pub fn meta_data(mut self, flag: MetaDataFlag) -> Self {
217 self.meta_data_flags.push(flag);
218
219 self
220 }
221
222 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 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(¶ms_list)
269 .build()?;
270
271 Ok(Request {
272 request,
273 client: &self.client.client,
274 })
275 }
276
277 pub async fn send(&self) -> Result<Response> {
280 self.build()?.send().await
281 }
282
283 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 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 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 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", 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}