toml-spanner 1.0.0

High Performance Toml parser and deserializer that preserves span information with fast compile times.
Documentation
#[allow(dead_code)]
pub struct TestCase<'a> {
    pub name: &'a str,
    pub source: Option<&'a str>,
    pub dest: Option<&'a str>,
    pub modified: Option<&'a str>,
    pub expected: Option<&'a str>,
}

#[allow(dead_code)]
impl<'a> TestCase<'a> {
    pub fn source(&self) -> &'a str {
        self.source
            .unwrap_or_else(|| panic!("test case {:?}: missing SOURCE field", self.name))
    }

    pub fn dest(&self) -> &'a str {
        self.dest
            .unwrap_or_else(|| panic!("test case {:?}: missing DEST field", self.name))
    }

    pub fn modified(&self) -> &'a str {
        self.modified
            .unwrap_or_else(|| panic!("test case {:?}: missing MODIFIED field", self.name))
    }

    pub fn expected(&self) -> &'a str {
        self.expected
            .unwrap_or_else(|| panic!("test case {:?}: missing EXPECTED field", self.name))
    }
}

pub fn parse_test_cases(data: &str) -> Vec<TestCase<'_>> {
    let mut cases = Vec::new();

    let mut case_starts: Vec<(usize, &str)> = Vec::new();
    for line in data.lines() {
        if let Some(name) = line.strip_prefix("#--- ") {
            let byte_offset = line.as_ptr() as usize - data.as_ptr() as usize;
            case_starts.push((byte_offset, name.trim()));
        }
    }

    for (idx, &(start, name)) in case_starts.iter().enumerate() {
        let line_end = start + data[start..].find('\n').unwrap_or(data.len() - start);
        let raw_start = (line_end + 1).min(data.len());
        let raw_end = if idx + 1 < case_starts.len() {
            case_starts[idx + 1].0
        } else {
            data.len()
        };
        let raw = &data[raw_start..raw_end];

        let mut source = None;
        let mut dest = None;
        let mut modified = None;
        let mut expected = None;

        if raw.contains("\n#-- ") || raw.starts_with("#-- ") {
            let mut field_starts: Vec<(usize, &str)> = Vec::new();
            for line in raw.lines() {
                if let Some(field_name) = line.strip_prefix("#-- ") {
                    let offset = line.as_ptr() as usize - raw.as_ptr() as usize;
                    field_starts.push((offset, field_name.trim()));
                }
            }

            for (fi, &(fstart, fname)) in field_starts.iter().enumerate() {
                let line_end = fstart + raw[fstart..].find('\n').unwrap_or(raw.len() - fstart);
                let content_start = (line_end + 1).min(raw.len());
                let content_end = if fi + 1 < field_starts.len() {
                    let next = field_starts[fi + 1].0;
                    if next > 0 && raw.as_bytes()[next - 1] == b'\n' {
                        next - 1
                    } else {
                        next
                    }
                } else {
                    let end = raw.len();
                    if end > 0 && raw.as_bytes()[end - 1] == b'\n' {
                        end - 1
                    } else {
                        end
                    }
                };
                let content = if content_start <= content_end {
                    &raw[content_start..content_end]
                } else {
                    ""
                };
                match fname {
                    "SOURCE" => source = Some(content),
                    "DEST" => dest = Some(content),
                    "MODIFIED" => modified = Some(content),
                    "EXPECTED" => expected = Some(content),
                    _ => panic!(
                        "test case {name:?}: unknown field {fname:?} (expected SOURCE, DEST, MODIFIED, or EXPECTED)"
                    ),
                }
            }
        } else {
            let trimmed = raw.strip_prefix('\n').unwrap_or(raw);
            let trimmed = trimmed.strip_suffix('\n').unwrap_or(trimmed);
            source = Some(trimmed);
        }

        cases.push(TestCase {
            name,
            source,
            dest,
            modified,
            expected,
        });
    }

    cases
}

pub fn run_cases(cases: &[TestCase<'_>], f: impl Fn(&TestCase<'_>)) {
    for case in cases {
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(case)));
        if let Err(e) = result {
            eprintln!("FAILED test case: {:?}", case.name);
            std::panic::resume_unwind(e);
        }
    }
}