use alloc::string::{String, ToString};
use nom::IResult;
use nom::Parser;
use nom::branch::alt;
use nom::bytes::streaming::{tag, take_until};
use nom::character::streaming::{char, multispace0, multispace1, satisfy};
use nom::combinator::{map, opt, recognize};
use nom::multi::many_m_n;
use nom::sequence::{delimited, pair, preceded};
use tracing::instrument;
#[instrument(skip(i), fields(len = i.len()))]
pub fn ws(i: &str) -> IResult<&str, &str> {
recognize(many_m_n(0, 4096, satisfy(char::is_whitespace))).parse(i)
}
#[instrument(skip(i), fields(len = i.len()))]
pub fn ws_complete(i: &str) -> IResult<&str, &str> {
nom::character::complete::multispace0(i)
}
pub fn ws1(i: &str) -> IResult<&str, &str> {
multispace1(i)
}
fn line_comment(i: &str) -> IResult<&str, ()> {
let (i, _) = alt((
preceded(tag("//"), take_until("\n")),
preceded(tag("--"), take_until("\n")),
))
.parse(i)?;
Ok((i, ()))
}
fn block_comment(i: &str) -> IResult<&str, ()> {
let (i, _) = delimited(tag("/*"), take_until("*/"), tag("*/")).parse(i)?;
Ok((i, ()))
}
pub fn comment(i: &str) -> IResult<&str, ()> {
alt((line_comment, block_comment)).parse(i)
}
pub fn skip_comments_and_ws(i: &str) -> IResult<&str, ()> {
let mut input = i;
loop {
let (rest, _) = multispace0(input)?;
input = rest;
match opt(comment).parse(input) {
Ok((rest, Some(_))) => input = rest,
Ok((_rest, None)) => return Ok((input, ())),
Err(e) => return Err(e),
}
}
}
fn ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == ':' || c == '.'
}
fn ident_first_char(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_'
}
#[instrument(skip(i), fields(len = i.len()))]
pub fn identifier(i: &str) -> IResult<&str, &str> {
recognize(pair(
satisfy(ident_first_char),
many_m_n(0, 512, satisfy(ident_char)),
))
.parse(i)
}
pub fn backtick_ident(i: &str) -> IResult<&str, &str> {
delimited(char('`'), take_until("`"), char('`')).parse(i)
}
#[instrument(skip(i), fields(len = i.len()))]
pub fn attr_or_ident(i: &str) -> IResult<&str, String> {
let (i, s) = alt((
map(backtick_ident, |s: &str| s.to_string()),
map(identifier, |s: &str| s.to_string()),
))
.parse(i)?;
Ok((i, s))
}
#[instrument(skip(i), fields(len = i.len()))]
pub fn string_literal(i: &str) -> IResult<&str, String> {
let (i, _) = char('\'').parse(i)?;
let mut s = String::new();
let mut input = i;
loop {
let (rest, part) = take_until("'").parse(input)?;
s.push_str(part);
input = rest;
if input.is_empty() {
return Err(nom::Err::Incomplete(nom::Needed::new(1)));
}
let (rest, c) = nom::character::streaming::anychar.parse(input)?;
if c == '\'' {
if rest.starts_with('\'') {
s.push('\'');
input = rest.get(1..).unwrap_or("");
} else if rest.is_empty() && part.is_empty() {
s.push('\'');
return Ok((rest, s));
} else {
return Ok((rest, s));
}
} else {
s.push(c);
input = rest;
}
}
}
fn number_char(c: char) -> bool {
c.is_ascii_digit() || c == '.' || c == 'e' || c == 'E' || c == '-' || c == '+'
}
#[instrument(skip(i), fields(len = i.len()))]
pub fn number_str(i: &str) -> IResult<&str, &str> {
recognize(many_m_n(1, 64, satisfy(number_char))).parse(i)
}
pub fn keyword(kw: &'static str) -> impl Fn(&str) -> IResult<&str, ()> {
move |i: &str| {
let (i, _) = nom::bytes::streaming::tag_no_case(kw).parse(i)?;
Ok((i, ()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tracing_test::traced_test;
#[traced_test]
#[test]
fn test_identifier() {
assert_eq!(identifier("appId "), Ok((" ", "appId")));
assert_eq!(
identifier("request.headers.x "),
Ok((" ", "request.headers.x"))
);
}
#[traced_test]
#[test]
fn test_string_literal() {
assert_eq!(string_literal("'hello'"), Ok(("", "hello".to_string())));
assert_eq!(string_literal("''"), Ok(("", "'".to_string())));
}
}