mecha10-cli-core 0.6.3

Mecha10 CLI core foundation — shared types, services, and utilities
Documentation
//! Layout convention for where a scaffolded project's bundled demo simulation images live under
//! `<project>/assets/images/` (asset layout consolidation follow-up to the "unsupported content
//! tyoe" MuJoCo crash fix).
//!
//! Two independent places need to agree on this exact convention so a scaffolded project ends
//! up with exactly one copy of each bundled demo image, never a silently duplicated one under
//! `simulation/environments/<env>/assets/...`:
//! - `scripts/release/build/package-templates.sh`, which stages the downloaded-templates tarball
//!   (statically, from `packages/mecha10-templates/templates/assets/`) - the single
//!   template-resolution path `mecha10 init` copies from (LAB-2034).
//! - `mecha10_cli::handlers::init::rewrite_bundled_image_source_paths`, which fixes up a copied
//!   `environment.json`'s `type: "image"` `source` fields to point at wherever the tarball above
//!   actually placed the file.
//!
//! # The rule
//!
//! - An image referenced by exactly one bundled environment's `type: "image"` objects is
//!   *environment-specific*: it lands at `assets/images/<env_name>/<basename>`.
//! - An image referenced by more than one bundled environment is *shared*: it lands at
//!   `assets/images/<basename>` directly - written/copied once, not once per environment.
//!
//! As of this writing only `basic_arena` is bundled, so every one of its demo images
//! (`aiko.jpg`/`phoebe.jpg`) lands in the env-specific tier at
//! `assets/images/basic_arena/`. This module's logic re-derives the tier from whatever set of
//! environments is actually present, so a second bundled environment sharing one of these files
//! would automatically start landing it in the shared tier.
//!
//! A user's own hand-authored image (e.g. dropped at `assets/images/my_photo.jpg` and referenced
//! from their own edited `environment.json`) is untouched by any of this - it was never part of
//! the bundled catalog, so it never appears in the `references` this module reasons about.

use std::collections::{HashMap, HashSet};

/// Count how many *distinct* environments reference each image basename, given every
/// `(environment_name, image_basename)` pair found across a set of bundled environments'
/// `type: "image"` objects.
pub fn count_referencing_environments<'a>(
    references: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> HashMap<String, HashSet<String>> {
    let mut usage: HashMap<String, HashSet<String>> = HashMap::new();
    for (env_name, basename) in references {
        usage
            .entry(basename.to_string())
            .or_default()
            .insert(env_name.to_string());
    }
    usage
}

/// Where a bundled demo image with the given `basename`, declared by `env_name`, belongs under a
/// project's canonical `assets/images/` tree - see the module docs for the shared-vs-env-specific
/// rule. `referencing_env_count` is the number of distinct environments that reference this
/// basename (from [`count_referencing_environments`]).
pub fn bundled_image_project_path(env_name: &str, basename: &str, referencing_env_count: usize) -> String {
    if referencing_env_count > 1 {
        format!("assets/images/{basename}")
    } else {
        format!("assets/images/{env_name}/{basename}")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn env_specific_image_used_by_only_one_environment() {
        let usage = count_referencing_environments([("basic_arena", "aiko.jpg"), ("basic_arena", "phoebe.jpg")]);

        assert_eq!(usage["aiko.jpg"].len(), 1);
        assert_eq!(
            bundled_image_project_path("basic_arena", "aiko.jpg", usage["aiko.jpg"].len()),
            "assets/images/basic_arena/aiko.jpg"
        );
    }

    #[test]
    fn shared_image_used_by_more_than_one_environment() {
        let usage = count_referencing_environments([("basic_arena", "logo.jpg"), ("obstacle_course", "logo.jpg")]);

        assert_eq!(usage["logo.jpg"].len(), 2);
        assert_eq!(
            bundled_image_project_path("basic_arena", "logo.jpg", usage["logo.jpg"].len()),
            "assets/images/logo.jpg"
        );
        assert_eq!(
            bundled_image_project_path("obstacle_course", "logo.jpg", usage["logo.jpg"].len()),
            "assets/images/logo.jpg"
        );
    }

    #[test]
    fn duplicate_references_from_the_same_environment_count_once() {
        // A single environment referencing the same basename twice (two billboards of the same
        // image) must not be mistaken for two distinct *environments* referencing it.
        let usage = count_referencing_environments([("basic_arena", "aiko.jpg"), ("basic_arena", "aiko.jpg")]);

        assert_eq!(usage["aiko.jpg"].len(), 1);
    }
}