use logos::Logos;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompOp {
Eq,
Ne,
Gt,
Ge,
Lt,
Le,
}
#[derive(Logos, Debug, Clone, PartialEq)]
#[logos(skip r"[ \t\r\n\f]+")] #[logos(skip "\u{feff}")] pub enum Token {
#[token("select", ignore(case))]
Select,
#[token("as", ignore(case))]
As,
#[token("from", ignore(case))]
From,
#[token("where", ignore(case))]
Where,
#[token("order", ignore(case))]
Order,
#[token("by", ignore(case))]
By,
#[token("desc", ignore(case))]
Desc,
#[token("descending", ignore(case))]
Descending,
#[token("asc", ignore(case))]
Asc,
#[token("ascending", ignore(case))]
Ascending,
#[token("limit", ignore(case))]
Limit,
#[token("offset", ignore(case))]
Offset,
#[token("distinct", ignore(case))]
Distinct,
#[token("version", ignore(case))]
Version,
#[token("latest_version", ignore(case))]
LatestVersion,
#[token("all_versions", ignore(case))]
AllVersions,
#[token("top", ignore(case))]
Top,
#[token("forward", ignore(case))]
Forward,
#[token("backward", ignore(case))]
Backward,
#[token("contains", |lex| lex.slice().to_owned(), ignore(case))]
Contains(String),
#[token("and", ignore(case))]
And,
#[token("or", ignore(case))]
Or,
#[token("not", ignore(case))]
Not,
#[token("exists", ignore(case))]
Exists,
#[token("like", ignore(case))]
Like,
#[token("matches", ignore(case))]
Matches,
#[token("count", ignore(case))]
Count,
#[token("min", ignore(case))]
Min,
#[token("max", ignore(case))]
Max,
#[token("sum", ignore(case))]
Sum,
#[token("avg", ignore(case))]
Avg,
#[token("terminology", ignore(case))]
Terminology,
#[token("true", ignore(case))]
True,
#[token("false", ignore(case))]
False,
#[token("null", ignore(case))]
Null,
#[token("=", |_| CompOp::Eq)]
#[token("!=", |_| CompOp::Ne)]
#[token(">=", |_| CompOp::Ge)]
#[token("<=", |_| CompOp::Le)]
#[token(">", |_| CompOp::Gt)]
#[token("<", |_| CompOp::Lt)]
Comparison(CompOp),
#[token("(")]
LeftParen,
#[token(")")]
RightParen,
#[token("[")]
LeftBracket,
#[token("]")]
RightBracket,
#[token("{")]
LeftCurly,
#[token("}")]
RightCurly,
#[token(",")]
Comma,
#[token("/")]
Slash,
#[token("*")]
Asterisk,
#[token(";")]
Semicolon,
#[token("-")]
Minus,
#[token("--", line_comment)]
DoubleDash,
#[regex(r"\$[a-zA-Z][a-zA-Z0-9_]*", |lex| lex.slice().to_owned())]
Parameter(String),
#[regex(r"id[0-9]+(\.[0-9]+)*", |lex| lex.slice().to_owned())]
IdCode(String),
#[regex(r"at[0-9]+(\.[0-9]+)*", |lex| lex.slice().to_owned())]
AtCode(String),
#[regex(
r"([a-zA-Z][a-zA-Z0-9_.\-]*::)?[a-zA-Z][a-zA-Z0-9_]*-[a-zA-Z][a-zA-Z0-9_]*-[a-zA-Z][a-zA-Z0-9_]*\.[a-zA-Z][a-zA-Z0-9_-]*\.v[0-9]+(\.[0-9]+)*((-rc|-alpha)(\.[0-9]+)?)?",
|lex| lex.slice().to_owned()
)]
ArchetypeHrid(String),
#[regex(
r"[a-zA-Z0-9._\-]+(\([a-zA-Z0-9._\-]+\))?::[a-zA-Z0-9._\-]+(\|[^|\[\]]+\|)?",
|lex| lex.slice().to_owned()
)]
TermCode(String),
#[regex(r"[a-zA-Z][a-zA-Z0-9+.\-]*://[^ \t\r\n{}]*", |lex| lex.slice().to_owned())]
Uri(String),
#[regex(
r"\{[ \t\r\n]*/(\\.|[^/\r\n])*/[ \t\r\n]*(;[ \t\r\n]*'([^'\\]|\\.)*')?[ \t\r\n]*\}",
|lex| lex.slice().to_owned()
)]
ContainedRegex(String),
#[regex(r"[0-9]+[eE][+\-]?[0-9]+", |lex| lex.slice().to_owned())]
SciInteger(String),
#[regex(r"[0-9]*\.[0-9]+[eE][+\-]?[0-9]+", |lex| lex.slice().to_owned())]
SciReal(String),
#[regex(r"[0-9]*\.[0-9]+", |lex| lex.slice().to_owned())]
Real(String),
#[regex(r"[0-9]+", |lex| lex.slice().to_owned())]
Integer(String),
#[regex(r"'([^'\\]|\\.)*'", |lex| lex.slice().to_owned())]
#[regex(r#""([^"\\]|\\.)*""#, |lex| lex.slice().to_owned())]
String(String),
#[regex(r"[a-zA-Z][a-zA-Z0-9_]*", |lex| lex.slice().to_owned())]
Identifier(String),
}
fn line_comment(lex: &mut logos::Lexer<Token>) -> logos::Filter<()> {
let rem = lex.remainder();
let is_comment = rem.is_empty() || rem.starts_with([' ', '\t', '\r', '\n']);
if is_comment {
let end = rem.find(['\r', '\n']).unwrap_or(rem.len());
lex.bump(end);
logos::Filter::Skip
} else {
logos::Filter::Emit(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpannedTokens {
tokens: Vec<Token>,
spans: Vec<std::ops::Range<usize>>,
}
impl SpannedTokens {
#[must_use]
pub fn tokens(&self) -> &[Token] {
&self.tokens
}
#[must_use]
pub fn spans(&self) -> &[std::ops::Range<usize>] {
&self.spans
}
#[cfg(feature = "federation")]
pub(crate) fn without(&self, removed: std::ops::Range<usize>) -> Self {
let (tokens, spans) = self
.tokens
.iter()
.zip(&self.spans)
.enumerate()
.filter(|(i, _)| !removed.contains(i))
.map(|(_, (token, span))| (token.clone(), span.clone()))
.unzip();
Self { tokens, spans }
}
#[must_use]
pub fn into_tokens(self) -> Vec<Token> {
self.tokens
}
#[must_use]
pub fn byte_span(&self, tokens: &std::ops::Range<usize>) -> std::ops::Range<usize> {
let after_last = self.spans.last().map_or(0, |span| span.end);
let start = self
.spans
.get(tokens.start)
.map_or(after_last, |span| span.start);
let end = if tokens.end > tokens.start {
tokens
.end
.checked_sub(1)
.and_then(|last| self.spans.get(last))
.map_or(start, |span| span.end)
} else {
start
};
start..end
}
}
pub fn lex(src: &str) -> Result<Vec<Token>, LexError> {
lex_spanned(src).map(SpannedTokens::into_tokens)
}
pub fn lex_spanned(src: &str) -> Result<SpannedTokens, LexError> {
let mut tokens = Vec::new();
let mut spans = Vec::new();
let mut lexer = Token::lexer(src);
while let Some(res) = lexer.next() {
match res {
Ok(tok) => {
tokens.push(tok);
spans.push(lexer.span());
}
Err(()) => {
return Err(LexError {
span: lexer.span(),
slice: lexer.slice().to_owned(),
});
}
}
}
Ok(SpannedTokens { tokens, spans })
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unexpected token {slice:?} at bytes {span:?}")]
pub struct LexError {
pub span: std::ops::Range<usize>,
pub slice: String,
}
#[cfg(test)]
mod tests {
use super::*;
fn toks(src: &str) -> Vec<Token> {
lex(src).unwrap_or_else(|e| panic!("lex failed: {e}"))
}
#[test]
fn keywords_are_case_insensitive() {
assert_eq!(toks("SELECT"), vec![Token::Select]);
assert_eq!(toks("select"), vec![Token::Select]);
assert_eq!(toks("Contains"), vec![Token::Contains("Contains".into())]);
}
#[test]
fn simple_select_from() {
let t = toks("SELECT c FROM COMPOSITION c");
assert_eq!(
t,
vec![
Token::Select,
Token::Identifier("c".into()),
Token::From,
Token::Identifier("COMPOSITION".into()),
Token::Identifier("c".into()),
]
);
}
#[test]
fn path_with_comparison_and_string() {
let t = toks("e/ehr_id/value = 'x'");
assert_eq!(
t,
vec![
Token::Identifier("e".into()),
Token::Slash,
Token::Identifier("ehr_id".into()),
Token::Slash,
Token::Identifier("value".into()),
Token::Comparison(CompOp::Eq),
Token::String("'x'".into()),
]
);
}
#[test]
fn node_codes_and_archetype_hrid() {
assert_eq!(toks("at0001"), vec![Token::AtCode("at0001".into())]);
assert_eq!(toks("id1.2.3"), vec![Token::IdCode("id1.2.3".into())]);
assert_eq!(
toks("openEHR-EHR-OBSERVATION.blood_pressure.v1"),
vec![Token::ArchetypeHrid(
"openEHR-EHR-OBSERVATION.blood_pressure.v1".into()
)]
);
}
#[test]
fn id_prefix_alone_is_identifier() {
assert_eq!(toks("id"), vec![Token::Identifier("id".into())]);
assert_eq!(
toks("attribute"),
vec![Token::Identifier("attribute".into())]
);
}
#[test]
fn numbers_and_params() {
assert_eq!(toks("42"), vec![Token::Integer("42".into())]);
assert_eq!(toks("3.14"), vec![Token::Real("3.14".into())]);
assert_eq!(toks("1e10"), vec![Token::SciInteger("1e10".into())]);
assert_eq!(toks("$ehrId"), vec![Token::Parameter("$ehrId".into())]);
}
#[test]
fn comparison_operators() {
assert_eq!(toks(">="), vec![Token::Comparison(CompOp::Ge)]);
assert_eq!(toks("!="), vec![Token::Comparison(CompOp::Ne)]);
assert_eq!(toks("<"), vec![Token::Comparison(CompOp::Lt)]);
}
#[test]
fn contains_query_shape() {
let t = toks("FROM EHR e CONTAINS COMPOSITION c");
assert_eq!(
t,
vec![
Token::From,
Token::Identifier("EHR".into()),
Token::Identifier("e".into()),
Token::Contains("CONTAINS".into()),
Token::Identifier("COMPOSITION".into()),
Token::Identifier("c".into()),
]
);
}
#[test]
fn hyphenated_term_codes_lex_as_one_token() {
assert_eq!(
toks("SNOMED-CT::1234"),
vec![Token::TermCode("SNOMED-CT::1234".into())]
);
assert_eq!(
toks("ISO_639-1::en"),
vec![Token::TermCode("ISO_639-1::en".into())]
);
assert_eq!(
toks("SNOMED-CT::1234|cyanosis|"),
vec![Token::TermCode("SNOMED-CT::1234|cyanosis|".into())]
);
assert_eq!(
toks("snomed-ct(3.1)::3415004"),
vec![Token::TermCode("snomed-ct(3.1)::3415004".into())]
);
}
#[test]
fn subtraction_is_not_a_term_code_regression() {
assert_eq!(
toks("a-b"),
vec![
Token::Identifier("a".into()),
Token::Minus,
Token::Identifier("b".into()),
]
);
assert_eq!(
toks("a - 1"),
vec![
Token::Identifier("a".into()),
Token::Minus,
Token::Integer("1".into()),
]
);
}
#[test]
fn archetype_hrid_version_suffixes_and_namespace_hyphen() {
assert_eq!(
toks("openEHR-EHR-OBSERVATION.blood_pressure.v1.0.0-rc.2"),
vec![Token::ArchetypeHrid(
"openEHR-EHR-OBSERVATION.blood_pressure.v1.0.0-rc.2".into()
)]
);
assert_eq!(
toks("openEHR-EHR-OBSERVATION.blood_pressure.v2-alpha"),
vec![Token::ArchetypeHrid(
"openEHR-EHR-OBSERVATION.blood_pressure.v2-alpha".into()
)]
);
assert_eq!(
toks("org-x::openEHR-EHR-OBSERVATION.blood_pressure.v1"),
vec![Token::ArchetypeHrid(
"org-x::openEHR-EHR-OBSERVATION.blood_pressure.v1".into()
)]
);
}
#[test]
fn line_comments_are_skipped() {
assert_eq!(
toks("SELECT c -- trailing comment\nFROM COMPOSITION c"),
vec![
Token::Select,
Token::Identifier("c".into()),
Token::From,
Token::Identifier("COMPOSITION".into()),
Token::Identifier("c".into()),
]
);
assert_eq!(toks("c --"), vec![Token::Identifier("c".into())]);
assert_eq!(toks("c --\n"), vec![Token::Identifier("c".into())]);
assert_eq!(
toks("--foo"),
vec![Token::DoubleDash, Token::Identifier("foo".into())]
);
}
#[test]
fn spans_skip_the_whitespace_between_tokens() {
let src = "SELECT c";
let stream = lex_spanned(src).unwrap_or_else(|e| panic!("lex failed: {e}"));
assert_eq!(stream.spans(), &[0..6, 9..10]);
assert_eq!(stream.byte_span(&(0..2)), 0..10);
assert_eq!(stream.byte_span(&(1..2)), 9..10);
}
#[test]
fn a_token_range_past_the_end_maps_to_the_end_of_the_source() {
let stream = lex_spanned("SELECT").unwrap_or_else(|e| panic!("lex failed: {e}"));
assert_eq!(stream.byte_span(&(1..1)), 6..6);
assert_eq!(stream.byte_span(&(0..0)), 0..0);
let empty = lex_spanned("").unwrap_or_else(|e| panic!("lex failed: {e}"));
assert_eq!(empty.byte_span(&(0..0)), 0..0);
}
#[test]
fn utf_bom_is_skipped() {
assert_eq!(toks("\u{feff}SELECT"), vec![Token::Select]);
}
#[test]
fn term_code_and_predicate_brackets() {
assert_eq!(
toks("local::at0001"),
vec![Token::TermCode("local::at0001".into())]
);
let t = toks("[at0001]");
assert_eq!(
t,
vec![
Token::LeftBracket,
Token::AtCode("at0001".into()),
Token::RightBracket
]
);
}
}