initiative_core/command/token/constructors.rs
1#![cfg_attr(not(any(test, feature = "integration-tests")), expect(dead_code))]
2
3use super::{Token, TokenType};
4use std::hash::Hash;
5
6/// Matches one or more of a set of tokens, in any order but without repetition.
7///
8/// # Examples
9///
10/// ```
11/// # use futures::StreamExt as _;
12/// # tokio_test::block_on(async {
13/// # let app_meta = initiative_core::test_utils::app_meta();
14/// use initiative_core::command::prelude::*;
15///
16/// let token = any_of([keyword("badger"), keyword("mushroom"), keyword("snake")]);
17///
18/// assert_eq!(
19/// vec![
20/// // "Ungreedy" version consuming only one token,
21/// FuzzyMatch::Overflow(
22/// TokenMatch::new(&token, vec![
23/// TokenMatch::from(&keyword("mushroom")),
24/// ]),
25/// " snake badger badger".into(),
26/// ),
27///
28/// // two tokens,
29/// FuzzyMatch::Overflow(
30/// TokenMatch::new(&token, vec![
31/// TokenMatch::from(&keyword("mushroom")),
32/// TokenMatch::from(&keyword("snake")),
33/// ]),
34/// " badger badger".into(),
35/// ),
36///
37/// // and all three tokens. The final word is repeated and so does not match.
38/// FuzzyMatch::Overflow(
39/// TokenMatch::new(&token, vec![
40/// TokenMatch::from(&keyword("mushroom")),
41/// TokenMatch::from(&keyword("snake")),
42/// TokenMatch::from(&keyword("badger")),
43/// ]),
44/// " badger".into(),
45/// ),
46/// ],
47/// token
48/// .match_input("mushroom snake badger badger", &app_meta)
49/// .collect::<Vec<_>>()
50/// .await,
51/// );
52/// # })
53/// ```
54pub fn any_of<V>(tokens: V) -> Token
55where
56 V: Into<Vec<Token>>,
57{
58 Token::new(TokenType::AnyOf(tokens.into()))
59}
60
61/// A variant of `any_of` with a marker assigned, making it easy to jump directly to the
62/// matched result within the token tree.
63///
64/// # Examples
65///
66/// ```
67/// # use futures::StreamExt as _;
68/// # tokio_test::block_on(async {
69/// # let app_meta = initiative_core::test_utils::app_meta();
70/// use initiative_core::command::prelude::*;
71///
72/// #[derive(Hash)]
73/// enum Marker {
74/// AnyOf,
75/// }
76///
77/// let query = "badger snake";
78/// let token = sequence([
79/// keyword("badger"),
80/// any_of_m(Marker::AnyOf, [keyword("mushroom"), keyword("snake")]),
81/// ]);
82/// let token_match = token.match_input_exact(query, &app_meta).next().await.unwrap();
83///
84/// assert_eq!(
85/// Some(&[TokenMatch::from(&keyword("snake"))][..]),
86/// token_match.find_marker(Marker::AnyOf).unwrap().meta_sequence(),
87/// );
88/// # })
89/// ```
90pub fn any_of_m<M, V>(marker: M, tokens: V) -> Token
91where
92 M: Hash,
93 V: Into<Vec<Token>>,
94{
95 Token::new_m(marker, TokenType::AnyOf(tokens.into()))
96}
97
98/// Matches all sequences of one or more words. Quoted phrases are treated as single words.
99///
100/// # Examples
101///
102/// ```
103/// # use futures::StreamExt as _;
104/// # tokio_test::block_on(async {
105/// # let app_meta = initiative_core::test_utils::app_meta();
106/// use initiative_core::command::prelude::*;
107///
108/// let token = any_phrase();
109///
110/// assert_eq!(
111/// vec![
112/// // Ungreedily matches the quoted phrase as a single token,
113/// FuzzyMatch::Overflow(
114/// TokenMatch::new(&token, "badger badger"),
115/// " mushroom snake ".into(),
116/// ),
117///
118/// // the first two "words",
119/// FuzzyMatch::Overflow(
120/// TokenMatch::new(&token, r#""badger badger" mushroom"#),
121/// " snake ".into(),
122/// ),
123///
124/// // and the whole phrase.
125/// FuzzyMatch::Exact(TokenMatch::new(&token, r#""badger badger" mushroom snake"#)),
126/// ],
127/// token
128/// .match_input(r#" "badger badger" mushroom snake "#, &app_meta)
129/// .collect::<Vec<_>>()
130/// .await,
131/// );
132/// # })
133/// ```
134pub fn any_phrase() -> Token {
135 Token::new(TokenType::AnyPhrase)
136}
137
138/// A variant of `any_phrase` with a marker assigned, making it easy to jump directly to the
139/// matched result within the token tree.
140///
141/// # Examples
142///
143/// ```
144/// # use futures::StreamExt as _;
145/// # tokio_test::block_on(async {
146/// # let app_meta = initiative_core::test_utils::app_meta();
147/// use initiative_core::command::prelude::*;
148///
149/// #[derive(Hash)]
150/// enum Marker {
151/// AnyPhrase,
152/// }
153///
154/// let query = "badger mushroom snake";
155/// let token = sequence([keyword("badger"), any_phrase_m(Marker::AnyPhrase)]);
156/// let token_match = token.match_input_exact(query, &app_meta).next().await.unwrap();
157///
158/// assert_eq!(
159/// Some("mushroom snake"),
160/// token_match.find_marker(Marker::AnyPhrase).unwrap().meta_phrase(),
161/// );
162/// # })
163/// ```
164pub fn any_phrase_m<M>(marker: M) -> Token
165where
166 M: Hash,
167{
168 Token::new_m(marker, TokenType::AnyPhrase)
169}
170
171/// Matches any single word.
172///
173/// # Examples
174///
175/// ```
176/// # use initiative_core::command::prelude::*;
177/// # use futures::StreamExt as _;
178/// # tokio_test::block_on(async {
179/// # let app_meta = initiative_core::test_utils::app_meta();
180/// let token = any_word();
181///
182/// assert_eq!(
183/// Some(TokenMatch::new(&token, "BADGER")),
184/// token
185/// .match_input_exact("BADGER", &app_meta)
186/// .next()
187/// .await,
188/// );
189/// # })
190/// ```
191pub fn any_word() -> Token {
192 Token::new(TokenType::AnyWord)
193}
194
195/// A variant of `any_word` with a marker assigned, making it easy to jump directly to the
196/// matched result within the token tree.
197///
198/// # Examples
199///
200/// ```
201/// # use futures::StreamExt as _;
202/// # tokio_test::block_on(async {
203/// # let app_meta = initiative_core::test_utils::app_meta();
204/// use initiative_core::command::prelude::*;
205///
206/// #[derive(Hash)]
207/// enum Marker {
208/// AnyWord,
209/// }
210///
211/// let query = "badger mushroom";
212/// let token = sequence([keyword("badger"), any_phrase_m(Marker::AnyWord)]);
213/// let token_match = token.match_input_exact(query, &app_meta).next().await.unwrap();
214///
215/// assert_eq!(
216/// Some("mushroom"),
217/// token_match.find_marker(Marker::AnyWord).unwrap().meta_phrase(),
218/// );
219/// # })
220/// ```
221pub fn any_word_m<M>(marker: M) -> Token
222where
223 M: Hash,
224{
225 Token::new_m(marker, TokenType::AnyWord)
226}
227
228/// A single keyword, matched case-insensitively.
229///
230/// # Examples
231///
232/// ```
233/// # use futures::StreamExt as _;
234/// # tokio_test::block_on(async {
235/// # let app_meta = initiative_core::test_utils::app_meta();
236/// use initiative_core::command::prelude::*;
237///
238/// let token = keyword("badger");
239///
240/// assert_eq!(
241/// Some(TokenMatch::from(&token)),
242/// token
243/// .match_input_exact("BADGER", &app_meta)
244/// .next()
245/// .await,
246/// );
247/// # })
248/// ```
249///
250/// ## Autocomplete
251///
252/// ```
253/// # use futures::StreamExt as _;
254/// # tokio_test::block_on(async {
255/// # let app_meta = initiative_core::test_utils::app_meta();
256/// use initiative_core::command::prelude::*;
257///
258/// let token = keyword("badger");
259///
260/// assert_eq!(
261/// Some(FuzzyMatch::Partial(
262/// TokenMatch::from(&token),
263/// Some("er".to_string()),
264/// )),
265/// token
266/// .match_input("badg", &app_meta)
267/// .next()
268/// .await,
269/// );
270/// # })
271/// ```
272pub fn keyword(keyword: &'static str) -> Token {
273 Token::new(TokenType::Keyword(keyword))
274}
275
276/// A variant of `keyword` with a marker assigned, making it easy to jump directly to the
277/// matched result within the token tree.
278///
279/// # Examples
280///
281/// ```
282/// # use futures::StreamExt as _;
283/// # tokio_test::block_on(async {
284/// # let app_meta = initiative_core::test_utils::app_meta();
285/// use initiative_core::command::prelude::*;
286///
287/// #[derive(Hash)]
288/// enum Marker {
289/// Keyword,
290/// }
291///
292/// let token = sequence([
293/// optional(keyword_m(Marker::Keyword, "badger")),
294/// keyword("mushroom"),
295/// ]);
296///
297/// // We can easily distinguish between the case when the keyword was matched
298/// let query = "badger mushroom";
299/// let token_match = token.match_input_exact(query, &app_meta).next().await.unwrap();
300/// assert!(token_match.contains_marker(Marker::Keyword));
301///
302/// // and when it wasn't.
303/// let query = "mushroom";
304/// let token_match = token.match_input_exact(query, &app_meta).next().await.unwrap();
305/// assert!(!token_match.contains_marker(Marker::Keyword));
306/// # })
307/// ```
308pub fn keyword_m<M>(marker: M, keyword: &'static str) -> Token
309where
310 M: Hash,
311{
312 Token::new_m(marker, TokenType::Keyword(keyword))
313}
314
315/// Matches exactly one of a set of possible keywords, case-insensitively.
316///
317/// # Examples
318///
319/// ```
320/// # use futures::StreamExt as _;
321/// # tokio_test::block_on(async {
322/// # let app_meta = initiative_core::test_utils::app_meta();
323/// use initiative_core::command::prelude::*;
324///
325/// let token = keyword_list(["badger", "mushroom", "snake"]);
326///
327/// // Only consumes one word, despite the repetition in the input.
328/// assert_eq!(
329/// vec![FuzzyMatch::Overflow(
330/// TokenMatch::new(&token, "badger"),
331/// " badger mushroom".into(),
332/// )],
333/// token
334/// .match_input("badger badger mushroom", &app_meta)
335/// .collect::<Vec<_>>()
336/// .await,
337/// );
338/// # })
339/// ```
340///
341/// ## Autocomplete
342///
343/// ```
344/// # use futures::StreamExt as _;
345/// # tokio_test::block_on(async {
346/// # let app_meta = initiative_core::test_utils::app_meta();
347/// use initiative_core::command::prelude::*;
348///
349/// let token = keyword_list(["badge", "badger"]);
350///
351/// assert_eq!(
352/// vec![
353/// // The input appears in the keyword list,
354/// FuzzyMatch::Exact(TokenMatch::new(&token, "badge")),
355///
356/// // but can also be completed to another word.
357/// FuzzyMatch::Partial(
358/// TokenMatch::new(&token, "badge"),
359/// Some("r".to_string()),
360/// ),
361/// ],
362/// token
363/// .match_input("badge", &app_meta)
364/// .collect::<Vec<_>>()
365/// .await,
366/// );
367/// # })
368/// ```
369pub fn keyword_list<V>(keywords: V) -> Token
370where
371 V: IntoIterator<Item = &'static str>,
372{
373 Token::new(TokenType::KeywordList(keywords.into_iter().collect()))
374}
375
376/// A variant of `any_word` with a marker assigned, making it easy to jump directly to the
377/// matched result within the token tree.
378///
379/// # Examples
380///
381/// ```
382/// # use futures::StreamExt as _;
383/// # tokio_test::block_on(async {
384/// # let app_meta = initiative_core::test_utils::app_meta();
385/// use initiative_core::command::prelude::*;
386///
387/// #[derive(Hash)]
388/// enum Marker {
389/// KeywordList,
390/// }
391///
392/// let query = "badger mushroom";
393/// let token = sequence([
394/// keyword("badger"),
395/// keyword_list_m(Marker::KeywordList, ["mushroom", "snake"]),
396/// ]);
397/// let token_match = token.match_input_exact(query, &app_meta).next().await.unwrap();
398///
399/// assert_eq!(
400/// Some("mushroom"),
401/// token_match.find_marker(Marker::KeywordList).unwrap().meta_phrase(),
402/// );
403/// # })
404/// ```
405pub fn keyword_list_m<M, V>(marker: M, keywords: V) -> Token
406where
407 M: Hash,
408 V: IntoIterator<Item = &'static str>,
409{
410 Token::new_m(
411 marker,
412 TokenType::KeywordList(keywords.into_iter().collect()),
413 )
414}
415
416/// Matches the name of a Thing found in the journal or recent entities.
417///
418/// # Examples
419///
420/// ```
421/// # use futures::StreamExt as _;
422/// # tokio_test::block_on(async {
423/// # let app_meta = initiative_core::test_utils::app_meta::with_test_data().await;
424/// use initiative_core::command::prelude::*;
425///
426/// let query = "odysseus";
427/// let token = name();
428/// let odysseus = app_meta.repository.get_by_name("Odysseus").await.unwrap();
429/// let token_match = token.match_input_exact(query, &app_meta).next().await.unwrap();
430///
431/// assert_eq!(TokenMatch::new(&token, odysseus.clone()), token_match);
432///
433/// // The matched Record can be accessed directly from the TokenMatch tree.
434/// assert_eq!(Some(&odysseus), token_match.meta_record());
435/// # })
436/// ```
437///
438/// ## Autocomplete
439///
440/// ```
441/// # use futures::StreamExt as _;
442/// # tokio_test::block_on(async {
443/// # let app_meta = initiative_core::test_utils::app_meta::with_test_data().await;
444/// use initiative_core::command::prelude::*;
445///
446/// let query = "ody";
447/// let token = name();
448/// let odysseus = app_meta.repository.get_by_name("Odysseus").await.unwrap();
449///
450/// assert_eq!(
451/// Some(FuzzyMatch::Partial(
452/// TokenMatch::new(&token, odysseus),
453/// Some("sseus".to_string()),
454/// )),
455/// token.match_input(query, &app_meta).next().await,
456/// );
457/// # })
458/// ```
459pub fn name() -> Token {
460 Token::new(TokenType::Name)
461}
462
463/// A variant of `name` with a marker assigned, making it easy to jump directly to the
464/// matched result within the token tree.
465///
466/// # Examples
467///
468/// ```
469/// # use futures::StreamExt as _;
470/// # tokio_test::block_on(async {
471/// # let app_meta = initiative_core::test_utils::app_meta::with_test_data().await;
472/// use initiative_core::command::prelude::*;
473///
474/// #[derive(Hash)]
475/// enum Marker {
476/// Name,
477/// }
478///
479/// let query = "hail odysseus";
480/// let token = sequence([keyword("hail"), name_m(Marker::Name)]);
481/// let odysseus = app_meta.repository.get_by_name("Odysseus").await.unwrap();
482///
483/// let token_match = token.match_input_exact(query, &app_meta).next().await.unwrap();
484///
485/// assert_eq!(
486/// Some(&odysseus),
487/// token_match.find_marker(Marker::Name).unwrap().meta_record(),
488/// );
489/// # })
490/// ```
491pub fn name_m<M>(marker: M) -> Token
492where
493 M: Hash,
494{
495 Token::new_m(marker, TokenType::Name)
496}
497
498/// Matches the input with and without the contained token.
499///
500/// # Examples
501///
502/// ```
503/// # use futures::StreamExt as _;
504/// # tokio_test::block_on(async {
505/// # let app_meta = initiative_core::test_utils::app_meta();
506/// use initiative_core::command::prelude::*;
507///
508/// let token = optional(keyword("badger"));
509///
510/// assert_eq!(
511/// vec![
512/// // Passes the input directly through to the overflow,
513/// FuzzyMatch::Overflow(TokenMatch::from(&token), "badger".into()),
514///
515/// // as well as the matched result if present.
516/// FuzzyMatch::Exact(TokenMatch::new(&token, TokenMatch::from(&keyword("badger")))),
517/// ],
518/// token
519/// .match_input("badger", &app_meta)
520/// .collect::<Vec<_>>()
521/// .await,
522/// );
523/// # })
524/// ```
525pub fn optional(token: Token) -> Token {
526 Token::new(TokenType::Optional(Box::new(token)))
527}
528
529/// A variant of `name` with a marker assigned, making it easy to jump directly to the
530/// matched result within the token tree.
531///
532/// # Examples
533///
534/// We can inspect the metadata of the `optional` token to see if a match occurred or not. More
535/// normally, the inner token would be marked and we would check to see if the marked token
536/// exists in the tree (`TokenMatch::contains_token`).
537///
538/// ```
539/// # use futures::StreamExt as _;
540/// # tokio_test::block_on(async {
541/// # let app_meta = initiative_core::test_utils::app_meta::with_test_data().await;
542/// use initiative_core::command::prelude::*;
543///
544/// #[derive(Hash)]
545/// enum Marker {
546/// Optional,
547/// }
548///
549/// let token = sequence([
550/// optional_m(Marker::Optional, keyword("badger")),
551/// keyword("mushroom"),
552/// ]);
553///
554/// // We can inspect the metadata to see that this case contains a match
555/// let query = "badger mushroom";
556/// let token_match = token.match_input_exact(query, &app_meta).next().await.unwrap();
557/// assert!(token_match.find_marker(Marker::Optional).unwrap().meta_single().is_some());
558///
559/// // and this case does not.
560/// let query = "mushroom";
561/// let token_match = token.match_input_exact(query, &app_meta).next().await.unwrap();
562/// assert!(token_match.find_marker(Marker::Optional).unwrap().meta_single().is_none());
563/// # })
564/// ```
565pub fn optional_m<M>(marker: M, token: Token) -> Token
566where
567 M: Hash,
568{
569 Token::new_m(marker, TokenType::Optional(Box::new(token)))
570}
571
572/// Matches exactly one of a set of possible tokens. The matched token will be included in the
573/// result.
574///
575/// # Examples
576///
577/// ```
578/// # use futures::StreamExt as _;
579/// # tokio_test::block_on(async {
580/// # let app_meta = initiative_core::test_utils::app_meta();
581/// use initiative_core::command::prelude::*;
582///
583/// let token = or([keyword("badger"), any_word()]);
584///
585/// assert_eq!(
586/// vec![
587/// // "badger" matches a provided keyword,
588/// FuzzyMatch::Overflow(
589/// TokenMatch::new(&token, TokenMatch::from(&keyword("badger"))),
590/// " badger".into(),
591/// ),
592///
593/// // but it satisfies the wildcard any_word() case as well.
594/// // It only ever matches a single token, so the second "badger" in the input is
595/// // never consumed.
596/// FuzzyMatch::Overflow(
597/// TokenMatch::new(&token, TokenMatch::new(&any_word(), "badger")),
598/// " badger".into(),
599/// ),
600/// ],
601/// token
602/// .match_input("badger badger", &app_meta)
603/// .collect::<Vec<_>>()
604/// .await,
605/// );
606/// # })
607/// ```
608pub fn or<V>(tokens: V) -> Token
609where
610 V: IntoIterator<Item = Token>,
611{
612 Token::new(TokenType::Or(tokens.into_iter().collect()))
613}
614
615/// A variant of `or` with a marker assigned.
616pub fn or_m<M, V>(marker: M, tokens: V) -> Token
617where
618 M: Hash,
619 V: IntoIterator<Item = Token>,
620{
621 Token::new_m(marker, TokenType::Or(tokens.into_iter().collect()))
622}
623
624/// Matches an exact sequence of tokens.
625///
626/// # Examples
627///
628/// ```
629/// # use initiative_core::command::prelude::*;
630/// # use futures::StreamExt as _;
631/// # tokio_test::block_on(async {
632/// # let app_meta = initiative_core::test_utils::app_meta();
633/// let token = sequence([keyword("badger"), keyword("mushroom"), keyword("snake")]);
634///
635/// // The first two keywords are matched, but the third is not present.
636/// assert_eq!(
637/// vec![FuzzyMatch::Partial(
638/// TokenMatch::new(&token, vec![
639/// TokenMatch::from(&keyword("badger")),
640/// TokenMatch::from(&keyword("mushroom")),
641/// ]),
642/// None,
643/// )],
644/// token
645/// .match_input("badger mushroom", &app_meta)
646/// .collect::<Vec<_>>()
647/// .await,
648/// );
649/// # })
650/// ```
651pub fn sequence<V>(tokens: V) -> Token
652where
653 V: Into<Vec<Token>>,
654{
655 Token::new(TokenType::Sequence(tokens.into()))
656}
657
658/// A variant of `sequence` with a marker assigned.
659pub fn sequence_m<M, V>(marker: M, tokens: V) -> Token
660where
661 M: Hash,
662 V: Into<Vec<Token>>,
663{
664 Token::new_m(marker, TokenType::Sequence(tokens.into()))
665}