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}