1use crate::response::{Response, WordElement};
2use crate::{DatamuseClient, Error, Result};
3use reqwest;
4use std::fmt::{self, Display, Formatter};
5
6#[derive(Debug)]
12pub struct RequestBuilder<'a> {
13 client: &'a DatamuseClient,
14 endpoint: EndPoint,
15 vocabulary: Vocabulary,
16 parameters: Vec<Parameter>,
17 topics: Vec<String>, meta_data_flags: Vec<MetaDataFlag>, }
20
21#[derive(Debug)]
23pub struct Request<'a> {
24 client: &'a reqwest::Client,
25 request: reqwest::Request,
26}
27
28#[derive(Clone, Copy, Debug)]
34pub enum EndPoint {
35 Words,
37 Suggest,
39}
40
41#[derive(Clone, Copy, Debug)]
46pub enum Vocabulary {
47 English,
49 Spanish,
51 EnglishWiki,
53}
54
55#[derive(Clone, Copy, Debug)]
60pub enum RelatedType {
61 NounModifiedBy,
63 AdjectiveModifier,
65 Synonym,
67 Trigger,
69 Antonym,
71 KindOf,
73 MoreGeneral,
75 Comprises,
77 PartOf,
79 Follower,
81 Predecessor,
83 Rhyme,
85 ApproximateRhyme,
87 Homophones,
89 ConsonantMatch,
91}
92
93#[derive(Clone, Copy, Debug)]
96pub enum MetaDataFlag {
97 Definitions,
99 PartsOfSpeech,
101 SyllableCount,
103 Pronunciation(PronunciationFormat),
106 WordFrequency,
108}
109
110#[derive(Clone, Copy, Debug)]
113pub enum PronunciationFormat {
114 Arpabet,
116 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), MetaData(Vec<MetaDataFlag>),
137 HintString(String), }
139
140impl<'a> RequestBuilder<'a> {
141 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 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 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 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 pub fn add_topic(mut self, word: &str) -> Self {
184 self.topics.push(String::from(word));
185
186 self
187 }
188
189 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 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 pub fn max_results(mut self, maximum: u16) -> Self {
209 self.parameters.push(Parameter::MaxResults(maximum));
210
211 self
212 }
213
214 pub fn meta_data(mut self, flag: MetaDataFlag) -> Self {
218 self.meta_data_flags.push(flag);
219
220 self
221 }
222
223 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 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(¶ms_list)
270 .build()?;
271
272 Ok(Request {
273 request,
274 client: &self.client.client,
275 })
276 }
277
278 pub async fn send(&self) -> Result<Response> {
281 self.build()?.send().await
282 }
283
284 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 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 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 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", 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}