rspyts-cli 0.4.4

Compiler and build orchestrator for rspyts
Documentation
mod python;
mod typescript;
pub(crate) mod util;

use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::Path;

use anyhow::{Context, Result};
use serde::Serialize;

use crate::LockedDependency;
use crate::config::{PythonConfig, TypeScriptConfig};
use crate::resolve::ResolvedContract;
use std::collections::BTreeMap;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ContractFile<'a> {
    schema_version: u32,
    fingerprint: &'a str,
    manifest: &'a rspyts::ir::Manifest,
    dependencies: &'a BTreeMap<String, LockedDependency>,
    hosts: &'a crate::LockedHosts,
}

pub fn contract(
    root: &Path,
    manifest: &rspyts::ir::Manifest,
    dependencies: &BTreeMap<String, LockedDependency>,
    hosts: &crate::LockedHosts,
    fingerprint: &str,
) -> Result<()> {
    write_json(
        &root.join("contract.json"),
        &ContractFile {
            schema_version: crate::LOCK_VERSION,
            fingerprint,
            manifest,
            dependencies,
            hosts,
        },
    )
}

pub fn python(
    root: &Path,
    config: &PythonConfig,
    contract: &ResolvedContract,
    fingerprint: &str,
) -> Result<()> {
    python::emit(root, config, contract, fingerprint)
}

pub fn typescript(
    root: &Path,
    config: &TypeScriptConfig,
    contract: &ResolvedContract,
    fingerprint: &str,
) -> Result<()> {
    typescript::emit(root, config, contract, fingerprint)
}

pub fn validate_static_typescript(contract: &ResolvedContract) -> Result<()> {
    typescript::validate_static_contract(contract)
}

pub(crate) fn static_typescript_type_ids(
    contract: &ResolvedContract,
) -> std::collections::BTreeSet<rspyts::ir::DefinitionId> {
    typescript::static_surface_type_ids(contract)
}

pub(crate) fn wire_typescript_surface(
    contract: &ResolvedContract,
) -> (
    std::collections::BTreeSet<rspyts::ir::DefinitionId>,
    std::collections::BTreeSet<usize>,
) {
    let constants = typescript::wire_constant_indices(contract);
    let types = typescript::wire_type_ids(contract, &constants);
    (types, constants)
}

pub(crate) fn write(path: &Path, source: &str) -> Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let mut file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(path)
        .with_context(|| {
            format!(
                "generated file {} collides with authored source",
                path.display()
            )
        })?;
    file.write_all(source.as_bytes())
        .with_context(|| format!("failed to write {}", path.display()))
}

pub(crate) fn write_json<T: Serialize>(path: &Path, value: &T) -> Result<()> {
    let mut source = serde_json::to_vec(value)?;
    source.push(b'\n');
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let mut file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(path)
        .with_context(|| {
            format!(
                "generated file {} collides with authored source",
                path.display()
            )
        })?;
    file.write_all(&source)
        .with_context(|| format!("failed to write {}", path.display()))
}

#[cfg(test)]
mod tests {
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::*;

    #[test]
    fn generated_files_never_overwrite_authored_source() {
        let root = std::env::temp_dir().join(format!(
            "rspyts-generated-collision-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        fs::create_dir_all(&root).unwrap();
        let path = root.join("module.py");
        fs::write(&path, "# authored\n").unwrap();

        let error = write(&path, "# generated\n").unwrap_err();
        assert!(error.to_string().contains("collides with authored source"));
        assert_eq!(fs::read_to_string(path).unwrap(), "# authored\n");
        fs::remove_dir_all(root).unwrap();
    }
}