a3s-use-core 0.2.8

Shared typed contracts for A3S Use domains
Documentation
use std::collections::BTreeSet;

use pulldown_cmark::{Event, HeadingLevel, Options, Parser, Tag, TagEnd};
use serde_yaml_ng::{Mapping, Value};

use crate::UseResult;

use super::{
    bundle_error, limit_error, path, OkfBundleDiagnostic, OkfConceptSummary, OkfDiagnosticCode,
    OkfFormatVersion,
};

pub(super) struct DocumentInspection {
    pub(super) concept: Option<OkfConceptSummary>,
    pub(super) diagnostics: Vec<OkfBundleDiagnostic>,
}

pub(super) fn inspect_document(
    path: &str,
    content: &[u8],
    format_version: OkfFormatVersion,
    bundle_paths: &BTreeSet<String>,
    max_links: u64,
) -> UseResult<DocumentInspection> {
    let text = std::str::from_utf8(content).map_err(|error| {
        bundle_error(format!(
            "OKF Markdown document '{path}' must be UTF-8: {error}"
        ))
    })?;
    let parsed = parse_frontmatter(path, text)?;
    let file_name = path.rsplit('/').next().unwrap_or(path);

    match file_name {
        "index.md" => inspect_index(path, parsed, format_version, bundle_paths, max_links),
        "log.md" => inspect_log(path, parsed, bundle_paths, max_links),
        _ => inspect_concept(path, parsed, bundle_paths, max_links),
    }
}

fn inspect_index(
    path: &str,
    parsed: ParsedDocument<'_>,
    format_version: OkfFormatVersion,
    bundle_paths: &BTreeSet<String>,
    max_links: u64,
) -> UseResult<DocumentInspection> {
    if let Some(frontmatter) = &parsed.frontmatter {
        if path != "index.md" {
            return Err(bundle_error(format!(
                "Only the bundle-root index.md may contain frontmatter; '{path}' does not."
            )));
        }
        let mapping = yaml_mapping(path, frontmatter)?;
        if mapping.len() != 1 {
            return Err(bundle_error(
                "The bundle-root index.md frontmatter may contain only okf_version.",
            ));
        }
        let Some(version) = mapping_string(mapping, "okf_version") else {
            return Err(bundle_error(
                "The bundle-root index.md frontmatter requires string okf_version.",
            ));
        };
        if version != format_version.as_str() {
            return Err(bundle_error(format!(
                "The bundle-root okf_version '{version}' does not match the declared format version '{}'.",
                format_version.as_str()
            )));
        }
    }
    let (_, diagnostics) = inspect_references(
        path,
        parsed.body,
        parsed.frontmatter.as_ref(),
        bundle_paths,
        max_links,
    )?;
    Ok(DocumentInspection {
        concept: None,
        diagnostics,
    })
}

fn inspect_log(
    path: &str,
    parsed: ParsedDocument<'_>,
    bundle_paths: &BTreeSet<String>,
    max_links: u64,
) -> UseResult<DocumentInspection> {
    if let Some(frontmatter) = &parsed.frontmatter {
        yaml_mapping(path, frontmatter)?;
    }
    validate_log_dates(path, parsed.body)?;
    let (_, diagnostics) = inspect_references(
        path,
        parsed.body,
        parsed.frontmatter.as_ref(),
        bundle_paths,
        max_links,
    )?;
    Ok(DocumentInspection {
        concept: None,
        diagnostics,
    })
}

fn inspect_concept(
    path: &str,
    parsed: ParsedDocument<'_>,
    bundle_paths: &BTreeSet<String>,
    max_links: u64,
) -> UseResult<DocumentInspection> {
    let frontmatter = parsed.frontmatter.as_ref().ok_or_else(|| {
        bundle_error(format!(
            "OKF concept '{path}' requires a YAML frontmatter block."
        ))
    })?;
    let mapping = yaml_mapping(path, frontmatter)?;
    let type_name = mapping_string(mapping, "type").ok_or_else(|| {
        bundle_error(format!(
            "OKF concept '{path}' requires a non-empty string type."
        ))
    })?;
    if type_name.trim() != type_name
        || type_name.is_empty()
        || type_name.len() > 256
        || type_name.chars().any(char::is_control)
    {
        return Err(bundle_error(format!(
            "OKF concept '{path}' requires a non-empty string type."
        )));
    }
    let (link_count, diagnostics) = inspect_references(
        path,
        parsed.body,
        Some(frontmatter),
        bundle_paths,
        max_links,
    )?;
    let id = path
        .strip_suffix(".md")
        .ok_or_else(|| bundle_error(format!("OKF concept '{path}' is not Markdown.")))?;
    Ok(DocumentInspection {
        concept: Some(OkfConceptSummary {
            id: id.to_string(),
            path: path.to_string(),
            type_name: type_name.to_string(),
            link_count,
        }),
        diagnostics,
    })
}

fn inspect_references(
    source_path: &str,
    body: &str,
    frontmatter: Option<&Value>,
    bundle_paths: &BTreeSet<String>,
    max_links: u64,
) -> UseResult<(u64, Vec<OkfBundleDiagnostic>)> {
    let mut references = markdown_references(body)
        .into_iter()
        .map(|target| (target, false))
        .collect::<Vec<_>>();
    if let Some(Value::Mapping(mapping)) = frontmatter {
        references.extend(frontmatter_references(mapping));
    }
    if references.len() as u64 > max_links {
        return Err(limit_error());
    }

    let mut dangling = BTreeSet::new();
    for (target, allow_scope_descriptor) in &references {
        let Some(resolved) = path::resolve_reference(source_path, target, *allow_scope_descriptor)?
        else {
            continue;
        };
        if !bundle_paths.contains(&resolved) {
            dangling.insert(resolved);
        }
    }
    let diagnostics = dangling
        .into_iter()
        .map(|target| OkfBundleDiagnostic {
            code: OkfDiagnosticCode::DanglingLink,
            path: source_path.to_string(),
            message: format!(
                "OKF document '{source_path}' references missing bundle target '{target}'."
            ),
            target,
        })
        .collect();
    Ok((references.len() as u64, diagnostics))
}

fn markdown_references(body: &str) -> Vec<String> {
    Parser::new_ext(body, Options::all())
        .filter_map(|event| match event {
            Event::Start(Tag::Link { dest_url, .. } | Tag::Image { dest_url, .. }) => {
                Some(dest_url.into_string())
            }
            _ => None,
        })
        .collect()
}

fn frontmatter_references(mapping: &Mapping) -> Vec<(String, bool)> {
    let mut references = Vec::new();
    for name in ["resource", "computation"] {
        if let Some(value) = mapping_string(mapping, name) {
            references.push((value.to_string(), false));
        }
    }
    for name in ["executor", "attester"] {
        let Some(Value::Mapping(contract)) = mapping_value(mapping, name) else {
            continue;
        };
        if let Some(value) = mapping_string(contract, "resource") {
            references.push((value.to_string(), false));
        }
    }
    let Some(Value::Sequence(sources)) = mapping_value(mapping, "sources") else {
        return references;
    };
    for source in sources {
        let Value::Mapping(source) = source else {
            continue;
        };
        if let Some(value) = mapping_string(source, "resource") {
            references.push((value.to_string(), true));
        }
    }
    references
}

fn mapping_value<'a>(mapping: &'a Mapping, name: &str) -> Option<&'a Value> {
    mapping.iter().find_map(|(key, value)| match key {
        Value::String(key) if key == name => Some(value),
        _ => None,
    })
}

fn mapping_string<'a>(mapping: &'a Mapping, name: &str) -> Option<&'a str> {
    match mapping_value(mapping, name) {
        Some(Value::String(value)) => Some(value),
        _ => None,
    }
}

fn yaml_mapping<'a>(path: &str, value: &'a Value) -> UseResult<&'a Mapping> {
    match value {
        Value::Mapping(mapping) => Ok(mapping),
        _ => Err(bundle_error(format!(
            "OKF document '{path}' frontmatter must be a YAML mapping."
        ))),
    }
}

struct ParsedDocument<'a> {
    frontmatter: Option<Value>,
    body: &'a str,
}

fn parse_frontmatter<'a>(path: &str, text: &'a str) -> UseResult<ParsedDocument<'a>> {
    let mut lines = text.split_inclusive('\n');
    let Some(first) = lines.next() else {
        return Ok(ParsedDocument {
            frontmatter: None,
            body: text,
        });
    };
    if trim_line(first) != "---" {
        return Ok(ParsedDocument {
            frontmatter: None,
            body: text,
        });
    }

    let frontmatter_start = first.len();
    let mut offset = frontmatter_start;
    let mut closing = None;
    for line in lines {
        if trim_line(line) == "---" {
            closing = Some((offset, offset + line.len()));
            break;
        }
        offset += line.len();
    }
    let Some((frontmatter_end, body_start)) = closing else {
        return Err(bundle_error(format!(
            "OKF document '{path}' has an unterminated YAML frontmatter block."
        )));
    };
    let value = serde_yaml_ng::from_str::<Value>(&text[frontmatter_start..frontmatter_end])
        .map_err(|error| {
            bundle_error(format!(
                "OKF document '{path}' has invalid YAML frontmatter: {error}"
            ))
        })?;
    Ok(ParsedDocument {
        frontmatter: Some(value),
        body: &text[body_start..],
    })
}

fn trim_line(line: &str) -> &str {
    line.trim_end_matches(['\r', '\n'])
}

fn validate_log_dates(path: &str, body: &str) -> UseResult<()> {
    let dates = log_date_headings(body);
    if dates.iter().any(|date| !valid_iso_date(date))
        || dates.windows(2).any(|pair| pair[0] <= pair[1])
    {
        return Err(bundle_error(format!(
            "OKF log '{path}' requires unique ISO 8601 date headings in newest-first order."
        )));
    }
    Ok(())
}

fn log_date_headings(body: &str) -> Vec<String> {
    let mut headings = Vec::new();
    let mut current = None;
    for event in Parser::new_ext(body, Options::all()) {
        match event {
            Event::Start(Tag::Heading {
                level: HeadingLevel::H2,
                ..
            }) => current = Some(String::new()),
            Event::End(TagEnd::Heading(HeadingLevel::H2)) => {
                if let Some(heading) = current.take() {
                    headings.push(heading.trim().to_string());
                }
            }
            Event::Text(value)
            | Event::Code(value)
            | Event::InlineMath(value)
            | Event::DisplayMath(value)
            | Event::Html(value)
            | Event::InlineHtml(value)
            | Event::FootnoteReference(value) => {
                if let Some(heading) = &mut current {
                    heading.push_str(&value);
                }
            }
            Event::SoftBreak | Event::HardBreak => {
                if let Some(heading) = &mut current {
                    heading.push(' ');
                }
            }
            _ => {}
        }
    }
    headings
}

fn valid_iso_date(value: &str) -> bool {
    let bytes = value.as_bytes();
    if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
        return false;
    }
    let Some(year) = parse_digits(&bytes[0..4]) else {
        return false;
    };
    let Some(month) = parse_digits(&bytes[5..7]) else {
        return false;
    };
    let Some(day) = parse_digits(&bytes[8..10]) else {
        return false;
    };
    let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
    let max_day = match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 if leap => 29,
        2 => 28,
        _ => return false,
    };
    (1..=max_day).contains(&day)
}

fn parse_digits(bytes: &[u8]) -> Option<u32> {
    bytes.iter().try_fold(0_u32, |value, byte| {
        byte.is_ascii_digit()
            .then(|| value * 10 + u32::from(byte - b'0'))
    })
}