axiomsync 0.1.6

Core data-processing engine for AxiomSync local retrieval runtime.
Documentation
use std::fs;
use std::path::Path;

use semver::{Version, VersionReq};

use super::{
    EPISODIC_DEPENDENCY_NAME, EPISODIC_REQUIRED_MAJOR, EPISODIC_REQUIRED_MANIFEST_PATH,
    EPISODIC_REQUIRED_MINOR, EPISODIC_REQUIRED_WORKSPACE_MEMBER, EpisodicLockDependency,
    EpisodicManifestDependency,
};
use crate::models::EpisodicSemverProbeResult;

pub(super) fn run_episodic_semver_probe(workspace_dir: &Path) -> EpisodicSemverProbeResult {
    let core_manifest = workspace_dir
        .join("crates")
        .join("axiomsync")
        .join("Cargo.toml");
    if !core_manifest.exists() {
        return EpisodicSemverProbeResult::from_error("missing_axiomsync_crate".to_string());
    }

    let manifest_text = match fs::read_to_string(&core_manifest) {
        Ok(value) => value,
        Err(err) => {
            return EpisodicSemverProbeResult::from_error(format!(
                "manifest_read_error={} path={}",
                err,
                core_manifest.display()
            ));
        }
    };
    let manifest_dep = match parse_manifest_episodic_dependency(&manifest_text) {
        Ok(dep) => dep,
        Err(reason) => return EpisodicSemverProbeResult::from_error(reason),
    };

    let workspace_manifest = workspace_dir
        .join("crates")
        .join("episodic")
        .join("Cargo.toml");
    if !workspace_manifest.exists() {
        return EpisodicSemverProbeResult::from_error(format!(
            "missing_episodic_workspace_member path={}",
            workspace_manifest.display()
        ));
    }

    let workspace_manifest_text = match fs::read_to_string(&workspace_manifest) {
        Ok(value) => value,
        Err(err) => {
            return EpisodicSemverProbeResult::from_error(format!(
                "workspace_manifest_read_error={} path={}",
                err,
                workspace_manifest.display()
            ));
        }
    };
    let workspace_version = match parse_package_version(&workspace_manifest_text) {
        Ok(value) => value,
        Err(reason) => return EpisodicSemverProbeResult::from_error(reason),
    };

    let lock_path = workspace_dir.join("Cargo.lock");
    if !lock_path.exists() {
        return EpisodicSemverProbeResult::from_error(format!(
            "missing_workspace_lockfile path={}",
            lock_path.display()
        ));
    }
    let lock_text = match fs::read_to_string(&lock_path) {
        Ok(value) => value,
        Err(err) => {
            return EpisodicSemverProbeResult::from_error(format!(
                "lockfile_read_error={} path={}",
                err,
                lock_path.display()
            ));
        }
    };
    let lock_dep = match parse_lockfile_episodic_dependency(&lock_text) {
        Ok(dep) => dep,
        Err(reason) => return EpisodicSemverProbeResult::from_error(reason),
    };

    let manifest_path_ok = manifest_dep
        .path
        .as_deref()
        .is_some_and(|path| path.trim() == EPISODIC_REQUIRED_MANIFEST_PATH);
    let workspace_version_ok = episodic_lock_version_contract_matches(&workspace_version);
    let manifest_req_ok = manifest_dep
        .version_req
        .as_deref()
        .is_some_and(|raw| episodic_manifest_req_contract_matches(raw, &workspace_version));
    let manifest_source_ok = manifest_dep.has_path && !manifest_dep.has_git && manifest_path_ok;
    let lock_version_ok = lock_dep.version.trim() == workspace_version.trim()
        && episodic_lock_version_contract_matches(&lock_dep.version);
    let lock_source_ok = lock_dep.source.is_none();

    let passed = manifest_req_ok
        && manifest_source_ok
        && workspace_version_ok
        && lock_version_ok
        && lock_source_ok;
    EpisodicSemverProbeResult {
        passed,
        error: None,
        manifest_req: manifest_dep.version_req,
        manifest_req_ok: Some(manifest_req_ok),
        manifest_path: manifest_dep.path,
        manifest_path_ok: Some(manifest_path_ok),
        manifest_uses_path: Some(manifest_dep.has_path),
        manifest_uses_git: Some(manifest_dep.has_git),
        manifest_source_ok: Some(manifest_source_ok),
        workspace_member_path: Some(EPISODIC_REQUIRED_WORKSPACE_MEMBER.to_string()),
        workspace_member_present: Some(true),
        workspace_version: Some(workspace_version),
        workspace_version_ok: Some(workspace_version_ok),
        lock_version: Some(lock_dep.version),
        lock_version_ok: Some(lock_version_ok),
        lock_source: lock_dep.source,
        lock_source_ok: Some(lock_source_ok),
    }
}

pub(super) fn parse_manifest_episodic_dependency(
    manifest: &str,
) -> std::result::Result<EpisodicManifestDependency, String> {
    let manifest_doc: toml::Value =
        toml::from_str(manifest).map_err(|err| format!("manifest_toml_parse_error={err}"))?;
    let dependencies = manifest_doc
        .get("dependencies")
        .and_then(toml::Value::as_table)
        .ok_or_else(|| "manifest_missing_dependencies_table".to_string())?;
    let episodic = dependencies
        .get(EPISODIC_DEPENDENCY_NAME)
        .ok_or_else(|| "missing_episodic_dependency".to_string())?;

    match episodic {
        toml::Value::String(version_req) => Ok(EpisodicManifestDependency {
            version_req: Some(version_req.to_string()),
            path: None,
            git_url: None,
            rev: None,
            has_path: false,
            has_git: false,
        }),
        toml::Value::Table(fields) => {
            let git_url = fields
                .get("git")
                .and_then(toml::Value::as_str)
                .map(str::to_string);
            let rev = fields
                .get("rev")
                .and_then(toml::Value::as_str)
                .map(str::to_string);
            let version_req = fields
                .get("version")
                .and_then(toml::Value::as_str)
                .map(str::to_string);
            let path = fields
                .get("path")
                .and_then(toml::Value::as_str)
                .map(str::to_string);

            if git_url.is_some() && rev.is_none() {
                return Err("episodic_dependency_missing_rev".to_string());
            }

            Ok(EpisodicManifestDependency {
                version_req,
                path,
                git_url,
                rev,
                has_path: fields.contains_key("path"),
                has_git: fields.contains_key("git"),
            })
        }
        _ => Err("episodic_dependency_unsupported_shape".to_string()),
    }
}

pub(super) fn parse_lockfile_episodic_dependency(
    lockfile: &str,
) -> std::result::Result<EpisodicLockDependency, String> {
    let lock_doc: toml::Value =
        toml::from_str(lockfile).map_err(|err| format!("lockfile_toml_parse_error={err}"))?;
    let packages = lock_doc
        .get("package")
        .and_then(toml::Value::as_array)
        .ok_or_else(|| "lockfile_missing_package_array".to_string())?;

    let mut candidates = Vec::<EpisodicLockDependency>::new();
    for package in packages {
        let Some(package_table) = package.as_table() else {
            continue;
        };
        let name = package_table
            .get("name")
            .and_then(toml::Value::as_str)
            .unwrap_or_default();
        if name != EPISODIC_DEPENDENCY_NAME {
            continue;
        }
        let version = package_table
            .get("version")
            .and_then(toml::Value::as_str)
            .ok_or_else(|| "lockfile_episodic_missing_version".to_string())?
            .to_string();
        let source = package_table
            .get("source")
            .and_then(toml::Value::as_str)
            .map(str::to_string);
        candidates.push(EpisodicLockDependency { version, source });
    }

    if candidates.is_empty() {
        return Err("missing_episodic_lock_entry".to_string());
    }
    if candidates.len() == 1 {
        return Ok(candidates.remove(0));
    }

    let required_candidates = candidates
        .iter()
        .filter(|candidate| {
            candidate.source.is_none() && episodic_lock_version_contract_matches(&candidate.version)
        })
        .cloned()
        .collect::<Vec<_>>();
    match required_candidates.len() {
        1 => Ok(required_candidates
            .into_iter()
            .next()
            .expect("single required candidate")),
        0 => Err("ambiguous_episodic_lock_entry_no_workspace_match".to_string()),
        _ => Err("ambiguous_episodic_lock_entry_multiple_workspace_matches".to_string()),
    }
}

fn parse_package_version(manifest: &str) -> std::result::Result<String, String> {
    let manifest_doc: toml::Value =
        toml::from_str(manifest).map_err(|err| format!("manifest_toml_parse_error={err}"))?;
    manifest_doc
        .get("package")
        .and_then(toml::Value::as_table)
        .and_then(|package| package.get("version"))
        .and_then(toml::Value::as_str)
        .map(str::to_string)
        .ok_or_else(|| "manifest_missing_package_version".to_string())
}

pub(super) fn episodic_manifest_req_contract_matches(raw: &str, workspace_version: &str) -> bool {
    let Ok(version) = Version::parse(workspace_version.trim()) else {
        return false;
    };
    VersionReq::parse(raw.trim()).is_ok_and(|requirement| {
        requirement.matches(&version)
            && version.major == EPISODIC_REQUIRED_MAJOR
            && version.minor == EPISODIC_REQUIRED_MINOR
    })
}

pub(super) fn episodic_lock_version_contract_matches(raw: &str) -> bool {
    Version::parse(raw.trim()).is_ok_and(|version| {
        version.major == EPISODIC_REQUIRED_MAJOR && version.minor == EPISODIC_REQUIRED_MINOR
    })
}

#[cfg(test)]
mod tests {
    use super::{episodic_manifest_req_contract_matches, parse_lockfile_episodic_dependency};

    #[test]
    fn parse_lockfile_episodic_dependency_prefers_workspace_entry_when_multiple_entries_exist() {
        let lockfile = r#"
[[package]]
name = "episodic"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "episodic"
version = "0.2.0"
"#;
        let parsed = parse_lockfile_episodic_dependency(lockfile).expect("parse lock dependency");
        assert_eq!(parsed.version, "0.2.0");
        assert_eq!(parsed.source, None);
    }

    #[test]
    fn parse_lockfile_episodic_dependency_rejects_multiple_entries_without_workspace_match() {
        let lockfile = r#"
[[package]]
name = "episodic"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "episodic"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
"#;
        let err = parse_lockfile_episodic_dependency(lockfile).expect_err("must reject ambiguity");
        assert_eq!(err, "ambiguous_episodic_lock_entry_no_workspace_match");
    }

    #[test]
    fn episodic_manifest_req_contract_matches_accepts_workspace_version_requirement() {
        assert!(episodic_manifest_req_contract_matches("0.2.2", "0.2.2"));
        assert!(episodic_manifest_req_contract_matches("^0.2.2", "0.2.2"));
        assert!(!episodic_manifest_req_contract_matches("0.1.0", "0.2.2"));
        assert!(!episodic_manifest_req_contract_matches("invalid", "0.2.2"));
    }
}