k0mmand3r/
lib.rs

1/*
2file: src/lib.rs
3
4Concepts:
5K0mmand is a framework for parsing slash verbs in a chat application.
6
7KmdLine is a single verb with three parts:
8    Kmd is the instruction or empty for content
9        * verbs are always lowercase
10        * verbs ALWAYS begin with a letter then zero or more alphanumeric characters
11    params is a vector of KmdParameter
12        KmdParameter is a label-value pair
13            label is a string that starts with a letter followed by zero or more alphanumeric characters
14            value is a KmdValueType
15                KmdValueType is a string, number, boolean, user, channel, or tag
16                the string can be either quoted single alphanumeric, OR a quoted string
17    content is a string
18
19feature-python enables pyo3 bindings
20feature-wasm enables typescript-wasm bindings
21
22
23🤓 Rust-lang winnow is parser+combinator tutorial found here:
24* https://docs.rs/winnow/latest/winnow/_tutorial/index.html#modules
25
26Summary of winnow:
27    “parsers”, functions that take an input and give back an output
28    “combinators”, functions that take parsers and combine them together!
29
30Parsers takes an input & returns a result:
31    Ok indicates the parser successfully found what it was looking for; or
32    - Parsers do more than just return a binary “success”/“failure” code.
33    - On success, the parser will return the processed data. The input will be left pointing to data that still needs processing
34    Err indicates the parser could not find what it was looking for.
35    - then there are multiple errors that could be returned
36
37Winnow uses the PResult<O> type.
38The Ok variant has output: O; whereas the Err variant stores an error.
39
40To combine parsers,
41    you'll need a common way to refer to them so you use the
42        Parser<I, O, E> trait with Parser::parse_next đź’ˇ this is the primary way to drive parsing forward.
43    You’ll note that I and O are parameterized – while most of the examples will be with &str (i.e. parsing a string); they do not have to be strings; nor do they have to be the same type (consider the simple example where I = &str, and O = u64 – this parses a string into an unsigned integer.)
44
45---
46
47Kommand Grammar:
48this is a string parser for "/slash" verbs
49
50any string should first be trimmed for whitespace on the front and back.
51    after trimming, if found the kommand must begin with a forward slash "/",
52    any string which doesn't begin with a / is returned as "content" (it has no verb)
53
54if a kommand is found then parse proceeds to parse the grammar of the verb
55here are the rules for parsing a kommand grammar:
56
57zero or more parameters will be found
58    --parameters are prefixed by a double dash "--"
59    parameters are always alphanumeric
60    if a parameter is followed by an = then it will have a value token
61    so --parameter or --parameter=value can be returned
62    the order of parameters is important and should be preserved in the structures
63        a parameter with no value is called a "tag"
64        a parameter with a value is a type "kvpair"
65    values can be of four types:
66        1. string
67        2. number
68        3. boolean
69        4. @user  (a user token begins with a literal "@" followed by a letter, followed by one or more alphanumeric or emoji characters
70        5. #channel (a channel token begins with a literal "#" followed by a letter, followed by one or more alphanumeric or emoji characters
71
72    these structures once parsed should be stored in an k0mmand3r result object
73
74Examples (one per line)
75```
76/verb --param1=value1 --param2=value2 --tag random content
77/anotherverb --user=@john_doe --channel=#general
78this is just content, no verb!
79```
80
81*/
82
83#![allow(non_snake_case)]
84#![allow(unused_imports)]
85#![allow(dead_code)]
86#![allow(unused_variables)]
87//use indexmap::IndexMap;
88
89use std::collections::HashMap;
90use decimal_rs::Decimal;
91
92#[cfg(feature = "lang-python")]
93use pyo3::prelude::*;
94
95use winnow::ascii::{alpha1, alphanumeric0, alphanumeric1, multispace0};
96use winnow::combinator::alt; // encapsulates if/then/else ladder pattern
97use winnow::combinator::opt; // basic if then else
98use winnow::combinator::preceded; // an easy way to discard the prefix, using a provided combinators
99use winnow::combinator::Recognize;
100use winnow::combinator::WithRecognized;
101use winnow::combinator::{delimited, repeat, separated, separated_pair, *};
102use winnow::error::ErrMode;
103use winnow::error::ErrorKind;
104use winnow::error::ParserError;
105use winnow::prelude::*;
106use winnow::seq;
107use winnow::stream::Stream; // choose between two parsers; and we’re happy with either being used.
108use winnow::token::one_of; // one_of(('0'..='9', 'a'..='f', 'A'..='F')).parse_next(input)
109use winnow::{PResult, Parser};
110use winnow::token::take_while;
111
112use serde_json::json;
113use serde::Serialize;
114
115#[cfg(not(target_arch = "wasm32"))]
116#[cfg(feature = "lang-python")]
117mod python;
118#[cfg(not(target_arch = "wasm32"))]
119#[cfg(feature = "lang-python")]
120pub use python::k0mmand3r;
121
122
123#[cfg(target_arch = "wasm32")]
124mod typescript_wasm;
125
126
127
128/* winnow parsers */
129fn parse_prefix_dash2x<'s>(input: &mut &'s str) -> PResult<&'s str> {
130    "--".parse_next(input)
131}
132
133fn parse_label<'i>(input: &mut &'i str) -> PResult<&'i str> {
134    // first character alpha, followed by zero or more alphanumeric
135    let label_parser = seq!((alpha1, alphanumeric0));
136    label_parser.recognize().parse_next(input)
137}
138
139fn parse_value_quoted<'i>(input: &mut &'i str) -> PResult<&'i str> {
140    // example: "a" or "1"
141    delimited('"', alphanumeric1, '"').parse_next(input)
142}
143
144fn parse_value_unquoted<'i>(input: &mut &'i str) -> PResult<&'i str> {
145    // example: a 1
146    alphanumeric1.parse_next(input)
147}
148
149fn parse_value_quote_agnostic<'i>(input: &mut &'i str) -> PResult<&'i str> {
150    // example: "a" or "1" or a 1
151    alt((
152        parse_value_quoted,   // Try parsing a quoted value first
153        parse_value_unquoted, // If that fails, try parsing an unquoted value
154    ))
155    .parse_next(input)
156}
157
158fn parse_slashcommand<'i>(input: &mut &'i str) -> PResult<&'i str> {
159    // strips the / from a command-verb label
160    preceded("/", parse_label).parse_next(input)
161}
162
163fn parse_KmdParameter<'i>(input: &mut &'i str) -> PResult<(&'i str, &'i str)> {
164    // Parse the prefix "--"
165    // separated_pair(parse_label, "=", parse_values).parse_next(input)
166    preceded(
167        parse_prefix_dash2x,
168        separated_pair(
169            parse_label,
170            opt(delimited(multispace0, '=', multispace0)), // Make value part optional
171            opt(parse_value_quote_agnostic),               // Optional value
172        ),
173    )
174    .map(|(label, value)| (label, value.unwrap_or(""))) // Default to empty string if no value
175    .parse_next(input)
176}
177
178/**
179 * Parse the content of a message
180 */
181fn parse_content<'i>(input: &mut &'i str) -> PResult<&'i str> {
182    let trimmed_input = input.trim();
183
184    if trimmed_input.starts_with('/') {
185        let error = winnow::error::ContextError::new();
186        Err(winnow::error::ErrMode::Backtrack(error))
187    } else {
188        // Find the start of the trimmed content within the original input
189        let start_index = input.find(trimmed_input).unwrap_or(0);
190        // Calculate the end index of the trimmed content
191        let end_index = start_index + trimmed_input.len();
192
193        // Create a slice of the original input from the start to the end of the trimmed content
194        let result = &input[start_index..end_index];
195
196        // Update input to the remaining part after the trimmed content
197        *input = &input[end_index..];
198
199        Ok(result)
200    }
201}
202
203
204#[derive(Debug, PartialEq, Eq, Serialize)]
205pub struct KmdParams<'i> {
206    kvs: HashMap<&'i str, &'i str>,
207}
208
209impl<'i> KmdParams<'i> {
210    pub fn parse(input: &mut &'i str) -> PResult<Self> {
211        let kvs =
212            separated(0.., parse_KmdParameter, terminated(' ', multispace0)).parse_next(input)?;
213
214        Ok(Self { kvs })
215    }
216}
217
218
219/* *********************** */
220
221
222#[derive(Debug, PartialEq, Serialize)]
223pub struct KmdLine<'i> {
224    // Verb of the command; None if it's just content
225    verb: Option<String>,
226    // Parameters of the command; None if there are no parameters
227    params: Option<KmdParams<'i>>,
228    // Content; None if there is no content
229    content: Option<String>,
230}
231
232impl<'i> KmdLine<'i> {
233    pub fn parse(input: &mut &'i str) -> PResult<Self> {
234        let trimmed_input = input.trim();
235
236        if trimmed_input.starts_with('/') {
237            // Parse the verb
238            let verb = Some(parse_slashcommand(input)?.to_string());
239
240            // Check if the remaining input is empty after parsing the verb
241            if input.trim().is_empty() {
242                // If yes, return with verb only, no params and content
243                return Ok(KmdLine {
244                    verb,
245                    params: None,
246                    content: None,
247                });
248            }
249
250            // Consume whitespace before parsing params
251            let _ = multispace0.parse_next(input)?;
252
253            // Parse parameters
254            let params = opt(KmdParams::parse).parse_next(input)?;
255
256            // Consume whitespace before parsing content
257            let _ = multispace0.parse_next(input)?;
258
259            // Parse remaining content
260            let content = opt(parse_content).parse_next(input)?.map(|c| c.to_string());
261
262            Ok(KmdLine {
263                verb,
264                params,
265                content,
266            })
267        } else {
268            // If it's not a verb, treat the entire input as content
269            // parse_content(input).map(|content| KmdLine::Content(content.to_string()))
270            let content = Some(parse_content(input)?.to_string());
271            Ok(KmdLine {
272                verb: None,
273                params: None,
274                content,
275            })
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn content_without_leading_slash() {
286        let mut input = "this is just content";
287        let expected = "this is just content";
288        let result = parse_content(&mut input).unwrap();
289        assert_eq!(result, expected);
290    }
291
292    #[test]
293    fn content_with_leading_slash() {
294        let mut input = "/not just content";
295        let result = parse_content(&mut input);
296        assert!(result.is_err()); // Expect an error because it starts with '/'
297    }
298
299    #[test]
300    fn content_with_whitespace() {
301        let mut input = "   leading and trailing spaces   ";
302        let expected = "leading and trailing spaces";
303        let result = parse_content(&mut input).unwrap();
304        assert_eq!(result, expected);
305    }
306
307    #[test]
308    fn test_parse_prefix_dash2x() {
309        let input = "--";
310        let actual = parse_prefix_dash2x.parse(input).unwrap();
311        let expected = "--";
312        assert_eq!(actual, expected)
313    }
314
315    #[test]
316    fn test_label() {
317        let input = "mylabel";
318        let actual = parse_label.parse(input).unwrap();
319        let expected = "mylabel";
320        assert_eq!(actual, expected)
321    }
322
323    #[test]
324    fn test_labelWithNumber() {
325        let input = "mylabel1";
326        let actual = parse_label.parse(input).unwrap();
327        let expected = "mylabel1";
328        assert_eq!(actual, expected)
329    }
330
331    #[test]
332    fn test_value_quoted() {
333        let input = r#""40""#;
334        let actual = parse_value_quoted.parse(input).unwrap();
335        let expected = "40";
336        assert_eq!(actual, expected)
337    }
338
339    #[test]
340    fn test_value_unquoted() {
341        let input = r#"40"#;
342        let actual = parse_value_unquoted.parse(input).unwrap();
343        let expected = "40";
344        assert_eq!(actual, expected)
345    }
346
347    #[test]
348    fn test_slashcommand() {
349        let input = r#"/command"#;
350        let actual = parse_slashcommand.parse(input).unwrap();
351        let expected = "command";
352        assert_eq!(actual, expected)
353    }
354
355    #[test]
356    fn test_isolatedLabel() {
357        let input = r#"--mylabel"#;
358        let actual = parse_KmdParameter.parse(input).unwrap();
359        let expected = ("mylabel", "");
360        assert_eq!(actual, expected)
361    }
362
363    #[test]
364    fn test_dash2xlabelvalueQUOTED_for_KmdParameter() {
365        let input = r#"--mylabel="40""#;
366        let actual = parse_KmdParameter.parse(input).unwrap();
367        let expected = ("mylabel", "40");
368        assert_eq!(actual, expected)
369    }
370
371    #[test]
372    fn test_dash2xlabelvalueUNQUOTED_for_KmdParameter() {
373        let input = r#"--mylabel=40"#;
374        let actual = parse_KmdParameter.parse(input).unwrap();
375        let expected = ("mylabel", "40");
376        assert_eq!(actual, expected)
377    }
378
379    #[test]
380    fn test_parametersOne() {
381        //let input = r#"--mylabel1="10" --mylabel2=20"#;
382        let input = r#"--onelabel="10""#;
383        let actual = KmdParams::parse.parse(input).unwrap();
384        let expected = KmdParams {
385            kvs: HashMap::from([("onelabel", "10")]),
386        };
387
388        assert_eq!(actual, expected)
389    }
390
391    #[test]
392    fn test_parametersMany() {
393        //let input = r#"--mylabel1="10" --mylabel2=20"#;
394        let input = r#"--mylabel="10" --yourlabel=20"#;
395        let actual = KmdParams::parse.parse(input).unwrap();
396        let expected = KmdParams {
397            kvs: HashMap::from([("mylabel", "10"), ("yourlabel", "20")]),
398        };
399
400        assert_eq!(actual, expected)
401    }
402
403    #[test]
404    fn test_kommand_WithContent() {
405        let mut input = r#"/save --mylabel=myvalue remaining content"#;
406        let actual = KmdLine::parse(&mut input).unwrap();
407        let expected = KmdLine {
408            verb: Some("save".to_string()),
409            params: Some(KmdParams {
410                kvs: HashMap::from([("mylabel", "myvalue")]),
411            }),
412            content: Some("remaining content".to_string()),
413        };
414        assert_eq!(actual, expected);
415    }
416
417    #[test]
418    fn test_kommand_ContentOnly() {
419        let mut input =
420            r#"lots of content with\nhard returns\nand embedded verbs and --parameters like /save"#;
421        let expected = KmdLine {
422            verb: None,
423            params: None,
424            content: Some(input.to_string()),
425        };
426
427        let actual = KmdLine::parse(&mut input).unwrap();
428        assert_eq!(actual, expected);
429    }
430
431
432    #[test]
433    fn test_kommand_VerbOnly() {
434        let mut input =
435            r#"/verbonly"#;
436        let expected = KmdLine {
437            verb: Some("verbonly".to_string()),
438            params: None,
439            content: None
440        };
441
442        let actual = KmdLine::parse(&mut input).unwrap();
443        assert_eq!(actual, expected);
444    }
445
446}
447
448
449/*
450
451FINAL INSTRUCTIONS:
452any errors will be pasted below solving those is 👍🏻,
453also solving any // TODO: blocks in the code!
454
455 */