sysml-v2-parser 0.17.0

SysML v2 textual notation parser for Rust
Documentation
//! Shared definition prelude: modifiers, keyword, `def`, identification, header.

use crate::ast::{Identification, Span};
use crate::parser::definition_header::parse_definition_header_after_ident;
use crate::parser::lex::{identification, ws1, ws_and_comments};
use crate::parser::Input;
use nom::bytes::complete::{tag, take_while1};
use nom::combinator::opt;
use nom::sequence::preceded;
use nom::IResult;
use nom::Parser;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DefKeywordMode {
    Required,
    Optional,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VisibilityPrefix {
    None,
    /// Optional `private` before the keyword (`constraint def`, etc.).
    OptionalPrivate,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnnotationMode {
    None,
    /// Leading `#identifier` (connection definitions).
    HashIdentifier,
}

#[derive(Debug, Clone, Copy)]
pub struct DefinitionPrefixOptions {
    pub keyword: &'static [u8],
    /// Second keyword after the first (`use` then `case`, etc.).
    pub second_keyword: Option<&'static [u8]>,
    pub def: DefKeywordMode,
    pub abstract_allowed: bool,
    pub visibility: VisibilityPrefix,
    pub annotation: AnnotationMode,
}

impl DefinitionPrefixOptions {
    pub const fn new(keyword: &'static [u8]) -> Self {
        Self {
            keyword,
            second_keyword: None,
            def: DefKeywordMode::Optional,
            abstract_allowed: true,
            visibility: VisibilityPrefix::None,
            annotation: AnnotationMode::None,
        }
    }

    pub const fn with_second_keyword(mut self, second: &'static [u8]) -> Self {
        self.second_keyword = Some(second);
        self
    }

    pub const fn def_required(mut self) -> Self {
        self.def = DefKeywordMode::Required;
        self
    }

    pub const fn no_abstract(mut self) -> Self {
        self.abstract_allowed = false;
        self
    }

    pub const fn with_private(mut self) -> Self {
        self.visibility = VisibilityPrefix::OptionalPrivate;
        self
    }

    pub const fn with_hash_annotation(mut self) -> Self {
        self.annotation = AnnotationMode::HashIdentifier;
        self
    }
}

#[derive(Debug, Clone)]
pub struct DefinitionPrefixResult {
    pub identification: Identification,
    pub specializes: Option<String>,
    pub specializes_span: Option<Span>,
    pub annotation: Option<String>,
    pub is_abstract: bool,
}

/// Parse from start of input through identification and optional subclassification header.
pub(crate) fn parse_definition_prefix(
    input: Input<'_>,
    options: DefinitionPrefixOptions,
) -> IResult<Input<'_>, DefinitionPrefixResult> {
    let (input, _) = ws_and_comments(input)?;

    let (input, annotation) = match options.annotation {
        AnnotationMode::None => (input, None),
        AnnotationMode::HashIdentifier => {
            let (input, raw) = opt(preceded(
                tag(&b"#"[..]),
                take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'_'),
            ))
            .parse(input)?;
            let annotation = raw.map(|span| String::from_utf8_lossy(span.fragment()).to_string());
            let (input, _) = ws_and_comments(input)?;
            (input, annotation)
        }
    };

    let input = match options.visibility {
        VisibilityPrefix::None => input,
        VisibilityPrefix::OptionalPrivate => {
            let (input, _) = opt(preceded(tag(&b"private"[..]), ws1)).parse(input)?;
            let (input, _) = opt(preceded(tag(&b"private"[..]), ws1)).parse(input)?;
            input
        }
    };

    let (input, is_abstract) = if options.abstract_allowed {
        let (input, found) = opt(preceded(tag(&b"abstract"[..]), ws1)).parse(input)?;
        (input, found.is_some())
    } else {
        (input, false)
    };

    let (input, _) = tag(options.keyword).parse(input)?;
    let (input, _) = ws1(input)?;
    let input = if let Some(second) = options.second_keyword {
        let (input, _) = tag(second).parse(input)?;
        let (input, _) = ws1(input)?;
        input
    } else {
        input
    };

    let input = match options.def {
        DefKeywordMode::Required => {
            let (input, _) = tag(&b"def"[..]).parse(input)?;
            let (input, _) = ws1(input)?;
            input
        }
        DefKeywordMode::Optional => opt(preceded(tag(&b"def"[..]), ws1)).parse(input)?.0,
    };

    let (input, identification) = identification(input)?;
    let (input, header) = parse_definition_header_after_ident(input)?;
    let specializes = header.specializes;
    let specializes_span = header.specializes_span;

    Ok((
        input,
        DefinitionPrefixResult {
            identification,
            specializes,
            specializes_span,
            annotation,
            is_abstract,
        },
    ))
}

#[cfg(test)]
mod tests {
    use super::*;
    use nom_locate::LocatedSpan;

    fn span_input(text: &str) -> Input<'_> {
        LocatedSpan::new(text.as_bytes())
    }

    #[test]
    fn prefix_parses_item_def_with_specializes() {
        let input = span_input("abstract item def Foo :> Base { }");
        let (rest, prefix) =
            parse_definition_prefix(input, DefinitionPrefixOptions::new(b"item")).expect("prefix");
        assert!(prefix.is_abstract);
        assert_eq!(prefix.identification.name.as_deref(), Some("Foo"));
        assert_eq!(prefix.specializes.as_deref(), Some("Base"));
        assert!(rest.fragment().trim_ascii_start().starts_with(b"{"));
    }

    #[test]
    fn prefix_parses_typed_library_header() {
        let input = span_input("connection connections : Connection[0..*] :> linkObjects, parts {");
        let (rest, prefix) = parse_definition_prefix(
            input,
            DefinitionPrefixOptions::new(b"connection").no_abstract(),
        )
        .expect("prefix");
        assert_eq!(prefix.identification.name.as_deref(), Some("connections"));
        assert_eq!(prefix.specializes.as_deref(), Some("linkObjects, parts"));
        assert!(rest.fragment().starts_with(b"{"));
    }

    #[test]
    fn prefix_parses_hash_annotation_connection() {
        let input = span_input("#MyConn abstract connection conn :> Base ;");
        let (rest, prefix) = parse_definition_prefix(
            input,
            DefinitionPrefixOptions::new(b"connection").with_hash_annotation(),
        )
        .expect("prefix");
        assert_eq!(prefix.annotation.as_deref(), Some("MyConn"));
        assert!(prefix.is_abstract);
        assert_eq!(prefix.specializes.as_deref(), Some("Base"));
        assert!(rest.fragment().trim_ascii_start().starts_with(b";"));
    }

    #[test]
    fn prefix_private_before_abstract_constraint() {
        let input = span_input("private abstract constraint def X ;");
        let (_, prefix) = parse_definition_prefix(
            input,
            DefinitionPrefixOptions::new(b"constraint")
                .with_private()
                .def_required(),
        )
        .expect("prefix");
        assert!(prefix.is_abstract);
        assert_eq!(prefix.identification.name.as_deref(), Some("X"));
    }

    #[test]
    fn prefix_required_def_individual() {
        let input = span_input("individual def X :> Y;");
        let (_, prefix) = parse_definition_prefix(
            input,
            DefinitionPrefixOptions::new(b"individual")
                .def_required()
                .no_abstract(),
        )
        .expect("prefix");
        assert!(!prefix.is_abstract);
        assert_eq!(prefix.specializes.as_deref(), Some("Y"));
    }
}