ssdcg 0.6.0

A service and data description format + a code generator based on rhai scripts
Documentation
use std::{collections::BTreeMap, num::ParseIntError, path::PathBuf};

use pest::{
    iterators::{Pair, Pairs},
    Parser, Span,
};
use pest_derive::Parser;
use regex::Regex;

use crate::ast::{
    Attribute, DataType, Dependency, Enum, EnumValue, Handler, Import, NameTypePair, Namespace,
    OrderedMap, Service, SsdcFile,
};

fn parse_attribute_arg(node: Pair<Rule>) -> Result<(String, Option<String>), ParseError> {
    let span = node.as_span();
    let mut p = node.into_inner();
    let name = p
        .next()
        .ok_or_else(|| ParseError::new(ParseErrorType::IncompleteAttributeArg, span))?
        .as_str()
        .to_string();
    let value = p.next().map(|p| p.into_inner().as_str().to_string());
    Ok((name, value))
}

fn parse_attribute(node: Pair<Rule>) -> Result<Attribute, ParseError> {
    let span = node.as_span();
    let mut p = node.into_inner();
    let name = p.next();
    let mut args = Vec::new();
    for p in p {
        args.push(parse_attribute_arg(p)?);
    }
    Ok(Attribute::new(
        Namespace::new(
            name.ok_or_else(|| ParseError::new(ParseErrorType::IncompleteAttribute, span))?
                .as_str(),
        ),
        args,
    ))
}

fn parse_attributes(node: Pair<Rule>) -> Result<Vec<Attribute>, ParseError> {
    node.into_inner().map(parse_attribute).collect()
}

fn parse_name(p: &mut Pairs<Rule>, n: Pair<Rule>) -> Result<(String, Vec<Attribute>), ParseError> {
    let span = n.as_span();
    if n.as_rule() == Rule::attributes {
        let attributes = parse_attributes(n)?;
        let name = p
            .next()
            .ok_or_else(|| ParseError::new(ParseErrorType::IncompleteName, span))?
            .as_str()
            .to_string();
        Ok((name, attributes))
    } else {
        let name = n.as_str().to_string();
        Ok((name, Vec::new()))
    }
}

#[derive(Parser)]
#[grammar = "grammar.pest"]
pub(crate) struct FileParser;

#[derive(Debug)]
pub struct ParseError {
    pub error_type: ParseErrorType,
    pub span: String,
}

impl ParseError {
    fn new(error_type: ParseErrorType, span: Span) -> Self {
        Self {
            error_type,
            span: format!("{:?}", span),
        }
    }
}

#[derive(Debug)]
pub enum ParseErrorType {
    IncompleteImport,
    IncompleteDatatype,
    IncompleteProperty,
    MissingType(String),
    IncompleteEnum,
    IncompleteEnumValue,
    InvalidEnumValue(String),
    IncompleteService,
    IncompleteDepends,
    IncompleteHandler,
    IncompleteArgumentIdent,
    IncompleteAttributeArg,
    IncompleteAttribute,
    IncompleteName,
    UnexpectedElement(String),
    OtherError(String),
}

impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match &self.error_type {
            ParseErrorType::IncompleteImport => write!(f, "Import incomplete. ({})", self.span),
            ParseErrorType::IncompleteDatatype => write!(f, "Datatype incomplete. ({})", self.span),
            ParseErrorType::IncompleteProperty => write!(f, "Property incomplete. ({})", self.span),
            ParseErrorType::MissingType(name) => {
                write!(f, "Type missing after {}. ({:?})", name, self.span)
            }
            ParseErrorType::IncompleteService => write!(f, "Service incomplete. ({})", self.span),
            ParseErrorType::IncompleteDepends => write!(f, "Depends incomplete. ({})", self.span),
            ParseErrorType::IncompleteHandler => write!(f, "Handler incomplete. ({})", self.span),
            ParseErrorType::IncompleteArgumentIdent => {
                write!(f, "Argument ident incomplete. ({})", self.span)
            }
            ParseErrorType::IncompleteAttributeArg => {
                write!(f, "Attribute argument incomplete. ({})", self.span)
            }
            ParseErrorType::IncompleteAttribute => {
                write!(f, "Attribute incomplete. ({})", self.span)
            }
            ParseErrorType::IncompleteName => {
                write!(f, "Name incomplete. ({})", self.span)
            }
            ParseErrorType::UnexpectedElement(info) => {
                write!(f, "Unexpected element {} ({})", info, self.span)
            }
            ParseErrorType::IncompleteEnum => write!(f, "Incomplete enum. ({})", self.span),
            ParseErrorType::IncompleteEnumValue => {
                write!(f, "Incomplete enum value. ({})", self.span)
            }
            ParseErrorType::InvalidEnumValue(info) => {
                write!(f, "Invalid enum value. {} ({})", info, self.span)
            }
            ParseErrorType::OtherError(inner) => {
                write!(f, "Other({})", inner)
            }
        }
    }
}

impl ParseError {
    fn from_dyn_error<T: std::error::Error>(err: T) -> Self {
        ParseError {
            error_type: ParseErrorType::OtherError(format!("{}", err)),
            span: String::new(),
        }
    }
}

impl std::error::Error for ParseError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        None
    }

    fn cause(&self) -> Option<&dyn std::error::Error> {
        self.source()
    }
}

#[allow(clippy::too_many_lines)]
pub fn parse(content: &str, namespace: Namespace) -> Result<SsdcFile, ParseError> {
    use ParseErrorType::{
        IncompleteArgumentIdent, IncompleteDatatype, IncompleteDepends, IncompleteEnum,
        IncompleteEnumValue, IncompleteHandler, IncompleteImport, IncompleteProperty,
        IncompleteService, InvalidEnumValue, MissingType, UnexpectedElement,
    };
    let pairs = FileParser::parse(Rule::file, content).map_err(ParseError::from_dyn_error)?;
    let mut imports = Vec::new();
    let mut datatypes = OrderedMap::new();
    let mut enums = OrderedMap::new();
    let mut services = OrderedMap::new();

    for p in pairs {
        match p.as_rule() {
            Rule::import => {
                let span = p.as_span();
                let mut p = p.into_inner();
                let n = p
                    .next()
                    .ok_or_else(|| ParseError::new(IncompleteImport, span))?;
                let (name, attributes) = parse_name(&mut p, n)?;
                imports.push(Import::new(Namespace::new(&name), attributes));
            }
            Rule::data => {
                let span = p.as_span();
                let mut p = p.into_inner();
                let n = p
                    .next()
                    .ok_or_else(|| ParseError::new(IncompleteDatatype, span))?;
                let (name, attributes) = parse_name(&mut p, n)?;

                let mut properties = OrderedMap::new();

                for p in p {
                    let span = p.as_span();
                    let mut p = p.into_inner();
                    let n = p
                        .next()
                        .ok_or_else(|| ParseError::new(IncompleteProperty, span))?;
                    let (name, attributes) = parse_name(&mut p, n)?;
                    let typ = p
                        .next()
                        .ok_or_else(|| ParseError::new(MissingType(name.clone()), span))?
                        .as_str()
                        .to_string();
                    properties.insert(name, NameTypePair::new(Namespace::new(&typ), attributes));
                }

                datatypes.insert(name, DataType::new(properties, attributes));
            }
            Rule::enum_ => {
                let span = p.as_span();
                let mut p = p.into_inner();
                let n = p
                    .next()
                    .ok_or_else(|| ParseError::new(IncompleteEnum, span))?;
                let (name, attributes) = parse_name(&mut p, n)?;

                let mut values = BTreeMap::new();

                for p in p {
                    let span = p.as_span();
                    let mut p = p.into_inner();
                    let n = p
                        .next()
                        .ok_or_else(|| ParseError::new(IncompleteEnumValue, span))?;
                    let (name, attributes) = parse_name(&mut p, n)?;
                    let value = if let Some(v) = p.next() {
                        Some(v.as_str().parse().map_err(|err: ParseIntError| {
                            ParseError::new(InvalidEnumValue(err.to_string()), span)
                        })?)
                    } else {
                        None
                    };
                    values.insert(name, EnumValue::new(value, attributes));
                }

                enums.insert(name, Enum::new(values, attributes));
            }
            Rule::service => {
                let span = p.as_span();
                let mut p = p.into_inner();
                let n = p
                    .next()
                    .ok_or_else(|| ParseError::new(IncompleteService, span))?;
                let (service_name, attributes) = parse_name(&mut p, n)?;

                let mut dependencies = Vec::new();
                let mut handlers = OrderedMap::new();

                for p in p {
                    match p.as_rule() {
                        Rule::depends => {
                            let span = p.as_span();
                            let mut p = p.into_inner();
                            let n = p
                                .next()
                                .ok_or_else(|| ParseError::new(IncompleteDepends, span))?;
                            let (name, attributes) = parse_name(&mut p, n)?;
                            dependencies.push(Dependency::new(Namespace::new(&name), attributes));
                        }
                        Rule::handler => {
                            let span = p.as_span();
                            let mut p = p.into_inner();
                            let n = p
                                .next()
                                .ok_or_else(|| ParseError::new(IncompleteHandler, span))?;
                            let (handler_name, handler_attributes) = parse_name(&mut p, n)?;
                            let mut arguments = OrderedMap::new();
                            let mut return_type = None;
                            let mut attributes = Vec::new();
                            for p in p.by_ref() {
                                match p.as_rule() {
                                    Rule::argument => {
                                        let span = p.as_span();
                                        let mut p = p.clone().into_inner();
                                        while let Some(n) = p.next() {
                                            match n.as_rule() {
                                                Rule::ident => {
                                                    let name = n.as_str().to_string();
                                                    let typ = p.next().ok_or_else(|| ParseError::new(IncompleteArgumentIdent, span))?.as_str().to_string();
                                                    arguments.insert(name, NameTypePair::new(Namespace::new(&typ), attributes.clone()));
                                                    attributes.clear();
                                                }
                                                Rule::attributes => {
                                                    attributes = parse_attributes(n)?;
                                                }
                                                _ => Err(ParseError::new(
                                                    UnexpectedElement(format!(
                                                        "while parsing argument for handler \"{}\" in service \"{}\"! {}",
                                                        handler_name,service_name, p
                                                    )),
                                                    span,
                                                ))?,
                                            }
                                        }
                                    }
                                    Rule::typ => {
                                        let re = Regex::new(r"\s+").expect("invalid regex");
                                        return_type =
                                            Some(Namespace::new(&re.replace_all(p.as_str(), " ")));
                                    }
                                    _ => Err(ParseError::new(
                                        UnexpectedElement(format!(
                                            "while parsing handler \"{}\" in service \"{}\"! {}",
                                            handler_name, service_name, p
                                        )),
                                        p.as_span(),
                                    ))?,
                                }
                            }

                            if let Some(p) = p.next() {
                                if p.as_rule() == Rule::typ {
                                    return_type = Some(Namespace::new(p.as_str()));
                                } else {
                                    Err(ParseError::new(
                                        UnexpectedElement(format!(
                                            "while parsing return type for handler \"{}\" in service \"{}\"! {}",
                                            handler_name,service_name, p
                                        )),
                                        p.as_span(),
                                    ))?;
                                }
                            }
                            handlers.insert(
                                handler_name,
                                Handler::new(arguments, return_type, handler_attributes),
                            );
                        }
                        _ => Err(ParseError::new(
                            UnexpectedElement(format!(
                                "while parsing service \"{}\"! {}",
                                service_name, p
                            )),
                            p.as_span(),
                        ))?,
                    }
                }

                services.insert(
                    service_name,
                    Service::new(dependencies, handlers, attributes),
                );
            }
            Rule::EOI => {}
            _ => Err(ParseError::new(
                UnexpectedElement(format!("{}", p)),
                p.as_span(),
            ))?,
        }
    }

    Ok(SsdcFile::new(
        namespace, imports, datatypes, enums, services,
    ))
}

pub fn parse_file(base: &PathBuf, path: PathBuf) -> Result<SsdcFile, ParseError> {
    let content = std::fs::read_to_string(&path).map_err(ParseError::from_dyn_error)?;

    let mut path = if path.starts_with(base) {
        path.strip_prefix(base)
            .map_err(ParseError::from_dyn_error)?
            .to_owned()
    } else {
        path
    };

    path.set_extension("");
    let components = path
        .components()
        .map(|c| c.as_os_str().to_string_lossy().to_string())
        .collect::<Vec<_>>();

    parse(&content, Namespace::from_vec(components))
}

#[test]
fn test_simple() {
    insta::assert_ron_snapshot!(parse(
        include_str!("../data/test.svc"),
        Namespace::new("__test__")
    )
    .unwrap());
}