rd-source 0.0.1

Parser for R documentation source files into rd-ast documents
Documentation
use std::{
    fs,
    path::{Path, PathBuf},
};

use rd_ast::{RdDocument, lower_r_object};
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SourceStatus {
    Valid,
}

#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum Comparison {
    #[default]
    OracleParity,
    IntentionalDivergence,
    SourceOnly,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct StringPair {
    pub kind: String,
    pub value: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Sequence {
    pub path: String,
    pub kinds: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Group {
    pub path: String,
    pub children: usize,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Arity {
    pub tag: String,
    pub counts: Vec<usize>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct OptionExpectation {
    pub tag: String,
    pub presence: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct OptionNode {
    pub kind: String,
    pub value: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct OptionNodes {
    pub tag: String,
    pub nodes: Vec<OptionNode>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ObligationSet {
    pub labels: Vec<String>,
    pub comments: usize,
    pub comment_paths: Option<Vec<String>>,
    pub comment_values: Option<Vec<String>>,
    pub expected_sequence: Option<Vec<Sequence>>,
    pub expected_leaves: Option<Vec<StringPair>>,
    pub expected_leaf_kinds: Option<Vec<String>>,
    pub expected_decoded: Option<Vec<StringPair>>,
    pub expected_groups: Option<Vec<Group>>,
    pub expected_root_nodes: Option<i64>,
    pub expected_arities: Option<Vec<Arity>>,
    pub expected_options: Option<Vec<OptionExpectation>>,
    pub expected_option_nodes: Option<Vec<OptionNodes>>,
    pub required_tags: Option<Vec<String>>,
}

#[derive(Debug, Clone, Copy)]
pub struct ObligationView<'a> {
    pub labels: &'a [String],
    pub comments: usize,
    pub comment_paths: Option<&'a [String]>,
    pub comment_values: Option<&'a [String]>,
    pub expected_sequence: Option<&'a [Sequence]>,
    pub expected_leaves: Option<&'a [StringPair]>,
    pub expected_leaf_kinds: Option<&'a [String]>,
    pub expected_decoded: Option<&'a [StringPair]>,
    pub expected_groups: Option<&'a [Group]>,
    pub expected_root_nodes: Option<i64>,
    pub expected_arities: Option<&'a [Arity]>,
    pub expected_options: Option<&'a [OptionExpectation]>,
    pub expected_option_nodes: Option<&'a [OptionNodes]>,
    pub required_tags: Option<&'a [String]>,
}

impl ObligationSet {
    fn view(&self) -> ObligationView<'_> {
        ObligationView {
            labels: &self.labels,
            comments: self.comments,
            comment_paths: self.comment_paths.as_deref(),
            comment_values: self.comment_values.as_deref(),
            expected_sequence: self.expected_sequence.as_deref(),
            expected_leaves: self.expected_leaves.as_deref(),
            expected_leaf_kinds: self.expected_leaf_kinds.as_deref(),
            expected_decoded: self.expected_decoded.as_deref(),
            expected_groups: self.expected_groups.as_deref(),
            expected_root_nodes: self.expected_root_nodes,
            expected_arities: self.expected_arities.as_deref(),
            expected_options: self.expected_options.as_deref(),
            expected_option_nodes: self.expected_option_nodes.as_deref(),
            required_tags: self.required_tags.as_deref(),
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
#[allow(dead_code)]
pub struct Case {
    pub name: String,
    pub category: String,
    pub rd: String,
    pub oracle: Option<String>,
    pub phase: String,
    pub source_status: Option<SourceStatus>,
    #[serde(default)]
    pub comparison: Comparison,
    pub divergence_reason: Option<String>,
    pub oracle_obligations: Option<ObligationSet>,
    pub source_obligations: Option<ObligationSet>,
    pub obligations: Vec<String>,
    pub comments: usize,
    pub comment_paths: Option<Vec<String>>,
    pub comment_values: Option<Vec<String>>,
    pub expected_sequence: Option<Vec<Sequence>>,
    pub expected_leaves: Option<Vec<StringPair>>,
    pub expected_leaf_kinds: Option<Vec<String>>,
    pub expected_decoded: Option<Vec<StringPair>>,
    pub expected_groups: Option<Vec<Group>>,
    pub expected_root_nodes: Option<i64>,
    pub expected_arities: Option<Vec<Arity>>,
    pub expected_options: Option<Vec<OptionExpectation>>,
    pub expected_option_nodes: Option<Vec<OptionNodes>>,
    pub required_tags: Option<Vec<String>>,
}

impl Case {
    pub fn flat_obligations(&self) -> ObligationView<'_> {
        ObligationView {
            labels: &self.obligations,
            comments: self.comments,
            comment_paths: self.comment_paths.as_deref(),
            comment_values: self.comment_values.as_deref(),
            expected_sequence: self.expected_sequence.as_deref(),
            expected_leaves: self.expected_leaves.as_deref(),
            expected_leaf_kinds: self.expected_leaf_kinds.as_deref(),
            expected_decoded: self.expected_decoded.as_deref(),
            expected_groups: self.expected_groups.as_deref(),
            expected_root_nodes: self.expected_root_nodes,
            expected_arities: self.expected_arities.as_deref(),
            expected_options: self.expected_options.as_deref(),
            expected_option_nodes: self.expected_option_nodes.as_deref(),
            required_tags: self.required_tags.as_deref(),
        }
    }

    pub fn oracle_obligations(&self) -> ObligationView<'_> {
        self.oracle_obligations
            .as_ref()
            .unwrap_or_else(|| panic!("{}: oracle obligations are missing", self.name))
            .view()
    }

    pub fn source_obligations(&self) -> ObligationView<'_> {
        self.source_obligations
            .as_ref()
            .unwrap_or_else(|| panic!("{}: source obligations are missing", self.name))
            .view()
    }

    pub fn is_source_only(&self) -> bool {
        self.comparison == Comparison::SourceOnly
    }
}

pub fn load_cases(root: &Path) -> Result<Vec<Case>, String> {
    let text = fs::read_to_string(root.join("cases.toml")).map_err(|e| e.to_string())?;
    parse_cases(&text)
}

pub fn parse_cases(text: &str) -> Result<Vec<Case>, String> {
    #[derive(Deserialize)]
    #[serde(deny_unknown_fields)]
    struct Manifest {
        case: Vec<Case>,
    }
    toml::from_str::<Manifest>(text)
        .map(|manifest| manifest.case)
        .map_err(|e| format!("invalid cases.toml: {e}"))
}

pub fn load_oracle(root: &Path, case: &Case) -> Result<RdDocument, String> {
    let path = root.join(
        case.oracle
            .as_ref()
            .ok_or_else(|| format!("{} has no oracle", case.name))?,
    );
    let bytes = fs::read(&path).map_err(|e| format!("{}: {e}", case.name))?;
    let object =
        rd_rds::parse(&bytes).map_err(|e| format!("{}: RDS parse failed: {e}", case.name))?;
    lower_r_object(&object).map_err(|e| format!("{}: lowering failed: {e}", case.name))
}

pub fn fixture_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}