whistle-proxy-rule-parser 0.1.5

Parse proxy rule set of whistle.
Documentation
use nom::character::is_space;
use nom::combinator::all_consuming;
use nom::error::{ErrorKind, ParseError};
use nom::Err::Error;
use nom::character::complete::none_of;
use nom::multi::separated_list0;
use nom::Parser;
use nom::{branch::alt, multi::many0, sequence::delimited};
use nom::{
    bytes::complete::{tag, take_until, take_till1, take_while, take_while1},
    character::complete::{multispace0, char as char1},
    combinator::{opt, map},
    sequence::{preceded, terminated, tuple},
    IResult,
};

pub mod markdown_values;

#[derive(Debug, Clone)]
pub struct Uri {
    pub scheme: String,
    pub host: String,
    pub path: String,
    pub query: String,
}

impl ToString for Uri {
    fn to_string(&self) -> String {
        format!("{}://{}{}{}", self.scheme, self.host, self.path, self.query)
    }
}
impl PartialEq for Uri {
    fn eq(&self, other: &Self) -> bool {
        self.scheme == other.scheme && self.host == other.host && self.path == other.path && self.query == other.query
    }
}

#[derive(Debug, Clone)]
pub enum OpValue {
    Inline(String),
    Value(String),
    Raw(String),
    TemplateString(TemplateString),
}

#[derive(Debug, Clone)]
pub struct Rule {
    pub name: String,
    pub value: OpValue,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplatePart {
    RawString(String),
    Value(String),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TemplateString {
    pub parts: Vec<TemplatePart>,
}

#[derive(Debug, Clone)]
pub struct ProxyRule {
  pub source: Uri,
  pub target: Uri,
  pub rules: Vec<Rule>,
}

#[derive(Debug, PartialEq)]
pub enum CustomError<I> {
  MyError,
  Nom(I, ErrorKind),
}

impl<I> ParseError<I> for CustomError<I> {
  fn from_error_kind(input: I, kind: ErrorKind) -> Self {
    CustomError::Nom(input, kind)
  }

  fn append(_: I, _: ErrorKind, other: Self) -> Self {
    other
  }
}

pub fn error_from_str(_input: &str) -> IResult<&str, &str, CustomError<&str>> {
  Err(Error(CustomError::MyError))
}


fn whitespace<'a, E: ParseError<&'a str>>(i: &'a str) -> IResult<&'a str, &'a str, E> {
  take_while1(|c: char| c.is_whitespace())(i)
}

fn not_space(s: &str) -> IResult<&str, &str> {
  take_while1(|c:char| !c.is_whitespace())(s)
}

pub fn parse_escaped(input: &str) -> IResult<&str, TemplatePart> {
    let (input, _) = tag("\\")(input)?;
    let (input, escaped) = none_of("\\")(input)?;
    Ok((input, TemplatePart::RawString(escaped.to_string())))
}

pub fn parse_template_string(input: &str) -> IResult<&str, TemplateString> {
    let original_input = input;
    let (mut input, bracket) = opt(char1('('))(input)?;
    if bracket.is_some() {
        input = input.strip_suffix(")").expect(&format!("{original_input} format is wrong"));
    }
    let (mut input, mut parts) = many0(
        nom::branch::alt((
            parse_escaped,
            map(preceded(tag("${"), terminated(take_until("}"), tag("}"))), |s: &str| TemplatePart::Value(s.to_string())),
            map(take_until("${"), |s: &str| TemplatePart::RawString(s.to_string())),
        )),
    )(input)?;

    if parts.is_empty() {
      // let chars: Vec<char> = input.chars().collect();
      // let s: String = chars[1..chars.len()-1].into_iter().collect();
      parts.push(TemplatePart::RawString(input.to_string()));
      input = "";
    }

    Ok((input, TemplateString { parts }))
}

pub fn parse_uri(input: &str) -> IResult<&str, Uri> {
    let (input, (scheme, host, path, query)) = tuple((
        opt(terminated(
            take_while1(|c: char| c.is_alphanumeric()),
            tag("://"),
        )),
        opt(take_while1(|c: char| c != '/')),
        take_while(|c: char| c != '?'),
        take_while(|c: char| !c.is_whitespace()),
    ))(input)?;

    Ok((
        input,
        Uri {
            scheme: scheme.unwrap_or_default().to_string(),
            host: host.unwrap_or_default().to_string(),
            path: path.to_string(),
            query: query.to_string(),
        },
    ))
}

pub fn parse_rule_value(input: &str) -> IResult<&str, OpValue> {
    let (input, opval) = alt((
        map(delimited(char1('`'), take_while(|c: char|c != ' ' && c != '\t' && c != '`'), char1('`')), |s:&str| OpValue::TemplateString(parse_template_string(s).unwrap().1)),
        map(delimited(char1('('), take_while(|c: char|c != ' ' && c != '\t' && c != ')'), char1(')')), |s:&str| OpValue::Inline(s.to_string())),
        map(delimited(char1('{'), take_while(|c: char|c != ' ' && c != '\t' && c != '}'), char1('}')), |s:&str| OpValue::Value(s.to_string())),
        map(take_while(|c:char| !is_space(c as u8) ), |s: &str| OpValue::Raw(s.to_string())),
    ))(input)?;

    Ok((
        input,
        opval,
    ))
}

pub fn parse_rule(input: &str) -> IResult<&str, Rule> {
    let (input, (name, value)) = tuple((
        terminated(take_while1(|c: char| c.is_alphanumeric()), tag("://")),
        map(take_while(|c: char| !c.is_whitespace()), |s:&str| parse_rule_value(s)),
    ))(input)?;

    let (_, value) = value?;

    Ok((
        input,
        Rule {
            name: name.to_string(),
            value,
        },
    ))
}

pub fn get_part(input: &str) -> IResult<&str, &str> {
    preceded(multispace0, take_till1(|c: char| c.is_whitespace()))(input)
}

pub fn get_rules(input: &str) -> IResult<&str, Vec<Rule>> {
  let (rest, rules) = preceded(whitespace, separated_list0(whitespace, map(not_space, |s:&str|  {
    parse_rule(s).unwrap().1
}))).parse(input)?;

  Ok((
    rest,
    rules,
  ))
}

// The error handler will trigger a 'static str reference, solution is here:
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=2de79a2b85310e11e915c674b28a9246
// Issue: https://github.com/rust-bakery/nom/issues/1571
pub fn parse_proxy_rule(input: &str) -> IResult<&str, ProxyRule> {
    let (rest, source) = map(get_part, all_consuming(parse_uri))(input)?;
    let source = source?.1;
    // println!("source: {:#?}", source);

    let (rest, target) = map(get_part, all_consuming(parse_uri))(rest)?;
    let target = target?.1;
    // println!("target: {:#?}", target);

    let (rest, rules) = if rest.trim().is_empty() {
      (rest, vec![])
    } else {
      get_rules(rest).unwrap()
    };

    Ok((
      rest,
      ProxyRule {
        source,
        target,
        rules,
      }
    ))
}

#[cfg(test)]
mod test {
  use super::*;
  #[test]
  fn test_uri_to_string(){
    let str = "http://localhost:8888/x?a=1";
    let (input, uri) = parse_uri(str).unwrap();
    assert_eq!(input, "");
    assert_eq!(uri, Uri {
      scheme: "http".into(),
      host: "localhost:8888".into(),
      path: "/x".into(),
      query: "?a=1".into(),
    });
    assert_eq!(uri.to_string(), str);
  }
  #[test]
  fn test_template_string(){
    let str = "`x=1&b=2`";
    let (input, ts) = parse_template_string(str).unwrap();
    assert_eq!(input, "");
    assert_eq!(ts.parts, vec![TemplatePart::RawString("`x=1&b=2`".into())]);

    // TODO: make below test remove ()
    let str = "`(x=1&b=2)`";
    let (input, ts) = parse_template_string(str).unwrap();
    assert_eq!(input, "");
    assert_eq!(ts.parts, vec![TemplatePart::RawString("`(x=1&b=2)`".into())]);
  }
}