rototo 0.1.0-alpha.6

Control plane for runtime configuration of your application.
Documentation
use std::sync::Arc;

use crate::error::Result;
use crate::sdk::{LoadOptions, Package};
use crate::source::SourceAuth;

pub(super) async fn get_runtime_package_from_inspected(
    inspected: Arc<Package>,
    source_token: &str,
) -> Result<Arc<Package>> {
    let inspected_root = inspected.root().to_string_lossy().into_owned();
    let runtime = Package::load_snapshot_with_options(
        inspected_root,
        load_options_for_source_token(source_token),
    )
    .await?;
    Ok(Arc::new(runtime))
}

fn load_options_for_source_token(source_token: &str) -> LoadOptions {
    if source_token.is_empty() {
        LoadOptions::default()
    } else {
        LoadOptions::default().with_source_auth(SourceAuth::Bearer(source_token.to_owned()))
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;
    use std::process::Command;

    use serde_json::json;
    use tempfile::TempDir;

    use super::*;
    use crate::console::stage::load;
    use crate::console::stage::{
        CachedPackageLocator, GitRefName, PackageLocator, PackagePath, SourceTreeOrigin,
        SourceTreeRevision, TokenIdentity,
    };
    use crate::sdk::EvaluationContext;

    #[tokio::test]
    async fn runtime_package_resolves_from_local_package_source() {
        let tree = TempDir::new().expect("tree tempdir");
        write_package(&tree.path().join("packages/payments")).await;

        let selector = cached_package_source(
            SourceTreeOrigin::local_folder(tree.path()).await.unwrap(),
            SourceTreeRevision::LocalWorkingTree,
            "packages/payments",
        );

        let inspected = load::get_inspected_package(selector, "").await.unwrap();
        let runtime = get_runtime_package_from_inspected(inspected, "")
            .await
            .unwrap();
        let resolved = runtime
            .resolve_variable(
                "checkout",
                &EvaluationContext::from_json(json!({})).expect("object context"),
            )
            .unwrap();

        assert_eq!(resolved.value, json!(true));
    }

    #[tokio::test]
    async fn runtime_package_resolves_from_git_revision_and_owns_staged_files() {
        let repo = TempDir::new().expect("repo tempdir");
        init_repo(repo.path());
        write_package(&repo.path().join("packages/payments")).await;
        commit_all(repo.path(), "add package");

        let selector = cached_package_source(
            SourceTreeOrigin::GitRemote {
                remote_url: format!("git+file://{}", repo.path().display()),
            },
            SourceTreeRevision::GitRef(GitRefName::new("main").unwrap()),
            "packages/payments",
        );

        let inspected = load::get_inspected_package(selector, "").await.unwrap();
        let runtime = get_runtime_package_from_inspected(inspected, "")
            .await
            .unwrap();

        assert!(
            runtime.root().join("rototo-package.toml").is_file(),
            "runtime package should own a snapshot of the inspected files"
        );
        let resolved = runtime
            .resolve_variable(
                "checkout",
                &EvaluationContext::from_json(json!({})).expect("object context"),
            )
            .unwrap();
        assert_eq!(resolved.value, json!(true));
    }

    #[tokio::test]
    async fn runtime_package_is_lint_gated_but_inspection_still_succeeds() {
        let tree = TempDir::new().expect("tree tempdir");
        write_lint_broken_package(&tree.path().join("packages/payments")).await;
        let selector = cached_package_source(
            SourceTreeOrigin::local_folder(tree.path()).await.unwrap(),
            SourceTreeRevision::LocalWorkingTree,
            "packages/payments",
        );

        let inspected = load::get_inspected_package(selector.clone(), "")
            .await
            .unwrap();
        assert!(inspected.root().join("rototo-package.toml").is_file());

        let err = get_runtime_package_from_inspected(inspected, "")
            .await
            .unwrap_err();
        assert!(err.to_string().contains("package lint failed"));
    }

    async fn write_package(path: &Path) {
        tokio::fs::create_dir_all(path.join("variables"))
            .await
            .unwrap();
        tokio::fs::write(path.join("rototo-package.toml"), "schema_version = 1\n")
            .await
            .unwrap();
        tokio::fs::write(
            path.join("variables/checkout.toml"),
            r#"
schema_version = 1
type = "bool"

[resolve]
default = true
"#
            .trim_start(),
        )
        .await
        .unwrap();
    }

    async fn write_lint_broken_package(path: &Path) {
        tokio::fs::create_dir_all(path.join("variables"))
            .await
            .unwrap();
        tokio::fs::write(path.join("rototo-package.toml"), "schema_version = 1\n")
            .await
            .unwrap();
        tokio::fs::write(
            path.join("variables/checkout.toml"),
            r#"
schema_version = 1
type = "bool"

[resolve]
default = "yes"
"#
            .trim_start(),
        )
        .await
        .unwrap();
    }

    fn cached_package_source(
        tree: SourceTreeOrigin,
        revision: SourceTreeRevision,
        path: &str,
    ) -> CachedPackageLocator {
        CachedPackageLocator::new(
            "user_123",
            PackageLocator::new(tree, revision, PackagePath::new(path).unwrap()),
            TokenIdentity::None,
        )
        .unwrap()
    }

    fn init_repo(path: &Path) {
        run_git(path, &["init", "-b", "main"]);
        run_git(path, &["config", "user.email", "console@example.com"]);
        run_git(path, &["config", "user.name", "Console Test"]);
    }

    fn commit_all(path: &Path, message: &str) {
        run_git(path, &["add", "."]);
        run_git(path, &["commit", "-m", message]);
    }

    fn run_git(path: &Path, args: &[&str]) {
        let status = Command::new("git")
            .args(args)
            .current_dir(path)
            .status()
            .expect("run git");
        assert!(status.success(), "git {:?} failed", args);
    }
}