diff --git a/questdb-confstr/README.md b/questdb-confstr/README.md index 2a89f85..6bdffd3 100644 --- a/questdb-confstr/README.md +++ b/questdb-confstr/README.md @@ -14,7 +14,7 @@ service::key1=value1;key2=value2;key3=value3; A few rules: * The last semicolon is mandatory. * Service name and keys are case-insensitive. -* Keys are ASCII alphanumeric and start with a letter. +* Keys are ASCII alphanumeric and can contain underscores. * Values are case-sensitive unicode strings which can contain any characters, * Except control characters (`0x00..=0x1f` and `0x7f..=0x9f`). * If semicolons `;` appears in a value, these are escaped as double semicolon `;;`. @@ -29,9 +29,8 @@ param ::= key "=" value key ::= identifier value ::= { value_char } -identifier ::= alpha { alphanumeric } -alpha ::= "a".."z" | "A".."Z" -alphanumeric ::= "a".."z" | "A".."Z" | "0".."9" +identifier ::= alpha_num_under { alpha_num_under } +alpha_num_under ::= "a".."z" | "A".."Z" | "0".."9" | "_" value_char ::= non_semicolon_char | escaped_semicolon escaped_semicolon ::= ";;" non_semicolon_char ::= ? any unicode character except ';', 0x00..=0x1f and 0x7f..=0x9f ? diff --git a/questdb-confstr/src/lib.rs b/questdb-confstr/src/lib.rs index c7246e6..b0c20e6 100644 --- a/questdb-confstr/src/lib.rs +++ b/questdb-confstr/src/lib.rs @@ -174,12 +174,10 @@ fn parse_ident( let mut token = String::new(); while let Some((pos, c)) = iter.peek0() { *next_pos = *pos; - let take = if token.is_empty() { - c.is_ascii_alphabetic() + if c.is_ascii_alphanumeric() || *c == '_' { + token.push(c.to_ascii_lowercase()); + iter.next(); } else { - c.is_ascii_alphanumeric() - }; - if !take { if token.is_empty() { return Err(parse_err(ErrorKind::ExpectedIdentifierNot(*c), *next_pos)); } else if !c.is_ascii() || matches!(c, '\0'..=' ') { @@ -187,8 +185,6 @@ fn parse_ident( } break; } - token.push(c.to_ascii_lowercase()); - iter.next(); } if token.is_empty() { diff --git a/questdb-confstr/tests/tests.rs b/questdb-confstr/tests/tests.rs index fb295b7..c304d9f 100644 --- a/questdb-confstr/tests/tests.rs +++ b/questdb-confstr/tests/tests.rs @@ -22,6 +22,7 @@ * ******************************************************************************/ +use std::collections::HashMap; use questdb_confstr::{parse_conf_str, ErrorKind, ParsingError}; #[test] @@ -75,17 +76,24 @@ fn duplicate_key() { } #[test] -fn key_must_start_with_letter() { +fn key_can_start_with_number() -> Result<(), ParsingError> { let input = "https::123=456;"; - let config = parse_conf_str(input); - assert!(config.is_err()); - let err = config.unwrap_err(); - assert_eq!(err.kind().clone(), ErrorKind::ExpectedIdentifierNot('1')); - assert_eq!(err.position(), 7); - assert_eq!( - err.to_string(), - "expected identifier to start with ascii letter, not '1' at position 7" - ); + let config = parse_conf_str(input)?; + assert_eq!(config.service(), "https"); + let mut expected = HashMap::new(); + expected.insert("123".to_string(), "456".to_string()); + assert_eq!(config.params(), &expected); + Ok(()) +} + +#[test] +fn identifiers_can_contain_underscores() -> Result<(), ParsingError> { + let input = "_A_::__x_Y__=42;"; + let config = parse_conf_str(input)?; + assert_eq!(config.service(), "_a_"); + let mut expected = HashMap::new(); + expected.insert("__x_y__".to_string(), "42".to_string()); + Ok(()) } #[test]