use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
Ident(String),
Number(f64),
LeftParen,
RightParen,
Comma,
Empty,
Eof,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WktError {
UnexpectedChar {
pos: usize,
ch: char,
},
UnexpectedToken {
expected: &'static str,
found: String,
},
UnexpectedEof,
InvalidNumber(String),
UnknownGeometryType(String),
TypeMismatch {
expected: &'static str,
found: &'static str,
},
NestingTooDeep,
}
impl core::fmt::Display for WktError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
WktError::UnexpectedChar { pos, ch } => {
write!(f, "unexpected character {ch:?} at byte {pos}")
}
WktError::UnexpectedToken { expected, found } => {
write!(f, "expected {expected}, found {found}")
}
WktError::UnexpectedEof => f.write_str("unexpected end of input"),
WktError::InvalidNumber(s) => write!(f, "invalid number {s:?}"),
WktError::UnknownGeometryType(s) => write!(f, "unknown geometry type {s:?}"),
WktError::TypeMismatch { expected, found } => {
write!(f, "type mismatch: expected {expected}, found {found}")
}
WktError::NestingTooDeep => {
f.write_str("WKT nesting too deep; exceeded the reader's recursion limit")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for WktError {}
pub(crate) fn tokenize(input: &str) -> Result<Vec<Token>, WktError> {
let mut tokens = Vec::new();
let mut chars = input.char_indices().peekable();
while let Some(&(pos, ch)) = chars.peek() {
if ch.is_whitespace() {
chars.next();
} else if ch == '(' {
chars.next();
tokens.push(Token::LeftParen);
} else if ch == ')' {
chars.next();
tokens.push(Token::RightParen);
} else if ch == ',' {
chars.next();
tokens.push(Token::Comma);
} else if ch.is_ascii_alphabetic() {
let mut word = String::new();
while let Some(&(_, c)) = chars.peek() {
if c.is_ascii_alphabetic() {
word.push(c.to_ascii_uppercase());
chars.next();
} else {
break;
}
}
if word == "EMPTY" {
tokens.push(Token::Empty);
} else {
tokens.push(Token::Ident(word));
}
} else if ch == '+' || ch == '-' || ch == '.' || ch.is_ascii_digit() {
let start = pos;
let mut end = pos + ch.len_utf8();
chars.next();
while let Some(&(p, c)) = chars.peek() {
if c.is_ascii_digit() || c == '.' || c == '+' || c == '-' || c == 'e' || c == 'E' {
end = p + c.len_utf8();
chars.next();
} else {
break;
}
}
let slice = &input[start..end];
let value: f64 = slice
.parse()
.map_err(|_| WktError::InvalidNumber(slice.to_string()))?;
tokens.push(Token::Number(value));
} else {
return Err(WktError::UnexpectedChar { pos, ch });
}
}
tokens.push(Token::Eof);
Ok(tokens)
}
#[cfg(test)]
mod tests {
#![allow(
clippy::float_cmp,
reason = "number tokens come from exact integer / short-decimal WKT literals"
)]
use super::{Token, WktError, tokenize};
use alloc::vec;
#[test]
fn each_token_kind() {
let toks = tokenize("POINT ( 1 , 2 )").unwrap();
assert_eq!(
toks,
vec![
Token::Ident("POINT".into()),
Token::LeftParen,
Token::Number(1.0),
Token::Comma,
Token::Number(2.0),
Token::RightParen,
Token::Eof,
]
);
}
#[test]
fn identifiers_are_uppercased() {
let toks = tokenize("LineString").unwrap();
assert_eq!(toks, vec![Token::Ident("LINESTRING".into()), Token::Eof]);
}
#[test]
fn empty_is_its_own_token() {
let toks = tokenize("POINT EMPTY").unwrap();
assert_eq!(
toks,
vec![Token::Ident("POINT".into()), Token::Empty, Token::Eof]
);
}
#[test]
fn dimension_suffix_lexes_as_ident() {
let toks = tokenize("POINT ZM").unwrap();
assert_eq!(
toks,
vec![
Token::Ident("POINT".into()),
Token::Ident("ZM".into()),
Token::Eof,
]
);
}
#[test]
fn number_e_notation() {
let toks = tokenize("1.5e-3").unwrap();
assert_eq!(toks, vec![Token::Number(0.0015), Token::Eof]);
}
#[test]
fn number_signed_and_decimal() {
let toks = tokenize("-10 20.5 +3").unwrap();
assert_eq!(
toks,
vec![
Token::Number(-10.0),
Token::Number(20.5),
Token::Number(3.0),
Token::Eof,
]
);
}
#[test]
fn malformed_char_reports_position() {
let err = tokenize("POINT (1 @)").unwrap_err();
assert_eq!(err, WktError::UnexpectedChar { pos: 9, ch: '@' });
}
#[test]
fn malformed_number_reports_slice() {
let err = tokenize("1.2.3").unwrap_err();
assert_eq!(err, WktError::InvalidNumber("1.2.3".into()));
}
}