pub fn tokenize(input: &str) -> Vec<String> {
let chars: Vec<char> = input.chars().collect();
let n = chars.len();
let mut tokens = Vec::new();
let mut i = 0;
while i < n {
while i < n && chars[i] == ' ' {
i += 1;
}
if i >= n {
break;
}
if chars[i] == '"' {
i += 1;
let mut token = String::new();
while i < n && chars[i] != '"' {
if chars[i] == '\\' && i + 1 < n {
let next = chars[i + 1];
if next == 'n' {
token.push('\n');
i += 2;
} else {
i += 1;
token.push(chars[i]);
i += 1;
}
} else {
token.push(chars[i]);
i += 1;
}
}
if i < n {
i += 1; }
tokens.push(token);
} else {
let mut token = String::new();
while i < n && chars[i] != ' ' {
if chars[i] == '"' {
token.push('"');
i += 1;
while i < n && chars[i] != '"' {
if chars[i] == '\\' && i + 1 < n {
let next = chars[i + 1];
if next == 'n' {
token.push('\n');
i += 2;
} else {
i += 1;
token.push(chars[i]);
i += 1;
}
} else {
token.push(chars[i]);
i += 1;
}
}
if i < n {
token.push('"');
i += 1;
}
} else {
token.push(chars[i]);
i += 1;
}
}
tokens.push(token.replace("\\n", "\n"));
}
}
tokens
}
pub fn is_key_value(token: &str) -> bool {
if token.starts_with('@') {
return false;
}
if is_arrow(token) {
return false;
}
let Some((key, value)) = token.split_once(':') else {
return false;
};
if key.is_empty() || value.is_empty() {
return false;
}
key.chars()
.all(|ch| matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-'))
}
pub fn parse_key_value(token: &str) -> (String, String) {
let (key, raw_value) = token.split_once(':').unwrap();
let value = raw_value
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(raw_value);
(key.to_string(), value.to_string())
}
pub fn parse_key_value_with_meta(token: &str) -> (String, String, bool) {
let (key, raw_value) = token.split_once(':').unwrap();
if let Some(unquoted) = raw_value.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
(key.to_string(), unquoted.to_string(), true)
} else {
(key.to_string(), raw_value.to_string(), false)
}
}
pub fn is_arrow(token: &str) -> bool {
token == "->" || token == "<->" || token == "--"
}
pub fn is_selector(token: &str) -> bool {
token.starts_with('@')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tokenize_simple_tokens() {
let got = tokenize("add svc AuthService");
assert_eq!(got, vec!["add", "svc", "AuthService"]);
}
#[test]
fn test_tokenize_quoted_strings() {
let got = tokenize(r#"add svc "Auth Service" theme:blue"#);
assert_eq!(got, vec!["add", "svc", "Auth Service", "theme:blue"]);
}
#[test]
fn test_tokenize_escaped_quotes() {
let got = tokenize(r#"label A "say \"hello\"""#);
assert_eq!(got, vec!["label", "A", r#"say "hello""#]);
}
#[test]
fn test_tokenize_empty_input() {
let got = tokenize("");
assert!(got.is_empty());
}
#[test]
fn test_tokenize_whitespace_only() {
let got = tokenize(" ");
assert!(got.is_empty());
}
#[test]
fn test_tokenize_multiple_spaces() {
let got = tokenize("add svc A");
assert_eq!(got, vec!["add", "svc", "A"]);
}
#[test]
fn test_tokenize_newline_in_unquoted() {
let got = tokenize(r"add svc Container\nRegistry");
assert_eq!(got, vec!["add", "svc", "Container\nRegistry"]);
}
#[test]
fn test_tokenize_newline_in_quoted() {
let got = tokenize(r#"add svc "Container\nRegistry""#);
assert_eq!(got, vec!["add", "svc", "Container\nRegistry"]);
}
#[test]
fn test_tokenize_embedded_quoted_value() {
let got = tokenize(r#"label:"Line1\nLine2""#);
assert_eq!(got, vec!["label:\"Line1\nLine2\""]);
}
#[test]
fn test_tokenize_multiple_newlines() {
let got = tokenize(r"add svc A\nB\nC");
assert_eq!(got, vec!["add", "svc", "A\nB\nC"]);
}
#[test]
fn test_tokenize_single_token() {
let got = tokenize("add");
assert_eq!(got, vec!["add"]);
}
#[test]
fn test_tokenize_empty_quoted_string() {
let got = tokenize(r#""""#);
assert_eq!(got, vec![""]);
}
#[test]
fn test_tokenize_unclosed_quote() {
let got = tokenize(r#""hello world"#);
assert_eq!(got, vec!["hello world"]);
}
#[test]
fn test_tokenize_escaped_backslash() {
let got = tokenize(r#""path\\dir""#);
assert_eq!(got, vec![r"path\dir"]);
}
#[test]
fn test_tokenize_colons_in_value() {
let got = tokenize("url:http://example.com");
assert_eq!(got, vec!["url:http://example.com"]);
}
#[test]
fn test_tokenize_unicode_labels() {
let got = tokenize("café 日本語 😀");
assert_eq!(got, vec!["café", "日本語", "😀"]);
}
#[test]
fn test_tokenize_unicode_quoted_label() {
let got = tokenize(r#"label "Café Étudiant""#);
assert_eq!(got, vec!["label", "Café Étudiant"]);
}
#[test]
fn test_is_key_value() {
let tests = vec![
("theme:blue", true),
("url:http://x", true),
("@type:db", false),
("->", false),
("hello", false),
("key:", false),
(":value", false),
];
for (input, want) in tests {
assert_eq!(
is_key_value(input),
want,
"is_key_value({:?}) = {}, want {}",
input,
is_key_value(input),
want
);
}
}
#[test]
fn test_parse_key_value() {
let (key, value) = parse_key_value("theme:blue");
assert_eq!(key, "theme");
assert_eq!(value, "blue");
let (key, value) = parse_key_value("url:http://x:8080");
assert_eq!(key, "url");
assert_eq!(value, "http://x:8080");
}
#[test]
fn test_parse_key_value_strips_quotes() {
let (key, value) = parse_key_value("label:\"Line1\nLine2\"");
assert_eq!(key, "label");
assert_eq!(value, "Line1\nLine2");
}
#[test]
fn test_parse_key_value_with_meta() {
let (key, value, was_quoted) = parse_key_value_with_meta(r#"engine_version:"15""#);
assert_eq!(key, "engine_version");
assert_eq!(value, "15");
assert!(was_quoted);
let (key, value, was_quoted) = parse_key_value_with_meta("port:80");
assert_eq!(key, "port");
assert_eq!(value, "80");
assert!(!was_quoted);
}
#[test]
fn test_is_arrow() {
assert!(is_arrow("->"));
assert!(is_arrow("<->"));
assert!(is_arrow("--"));
assert!(!is_arrow("=>"));
assert!(!is_arrow("add"));
}
#[test]
fn test_is_selector() {
assert!(is_selector("@type:db"));
assert!(is_selector("@all"));
assert!(!is_selector("type:db"));
assert!(!is_selector("add"));
}
}