bevy_brp_mcp 0.22.2

MCP server for Bevy Remote Protocol (BRP) integration
//! Collection strategy trait for app listing handlers

use std::collections::HashSet;
use std::path::PathBuf;

use serde_json::Value;
use serde_json::json;

use super::cargo_detector::BevyTarget;
use super::cargo_detector::BrpLevel;
use super::cargo_detector::CargoDetector;
use super::constants::BRP_LEVEL_FIELD;
use super::constants::BUILD_BUILT_FIELD;
use super::constants::BUILDS_FIELD;
use super::constants::KIND_FIELD;
use super::constants::NAME_FIELD;
use super::constants::PACKAGE_NAME_FIELD;
use super::constants::PATH_FIELD;
use super::constants::RELATIVE_PATH_FIELD;
use super::constants::WORKSPACE_ROOT_FIELD;
use super::scanning;
use crate::app_tools::constants::MANIFEST_PATH_FIELD;
use crate::app_tools::constants::PROFILE_DEBUG;
use crate::app_tools::constants::PROFILE_RELEASE;

/// Unified strategy for collecting all Bevy targets (apps and examples)
/// with `kind` and `brp_level` fields on each item.
///
/// Only targets declared in workspace `Cargo.toml` files are included (via `cargo metadata`).
pub(super) struct AllBevyTargetsStrategy;

/// A `BevyTarget` enriched with BRP status.
///
/// For bins, `brp_level` reflects whether the package's `src/` tree uses BRP plugins.
/// For examples, the individual source file is checked for BRP plugin imports.
pub(super) struct EnrichedTarget {
    pub(super) target:    BevyTarget,
    pub(super) brp_level: BrpLevel,
}

impl AllBevyTargetsStrategy {
    pub(super) fn collect_items(detector: &CargoDetector) -> Vec<EnrichedTarget> {
        let all_targets = detector.find_bevy_targets();

        // Build a package-level BRP lookup for bins (requires deep `src/` scan)
        let brp_targets = detector.find_brp_targets();
        let brp_keys: HashSet<String> = brp_targets
            .iter()
            .map(|t| format!("{}::{}", t.manifest.display(), t.name))
            .collect();

        // Enrich each target with BRP level using a hybrid approach:
        // - Bins: package-level set lookup (scans `src/` tree for BRP plugin registration) then
        //   file-level check for extras vs brp_only distinction
        // - Examples: per-file check (reads the example's source file directly)
        all_targets
            .into_iter()
            .map(|target| {
                let brp_level = if target.is_app() {
                    let key = format!("{}::{}", target.manifest.display(), target.name);
                    if brp_keys.contains(&key) {
                        // Package has BRP — check the specific binary's source for level
                        CargoDetector::file_brp_level(&target.source)
                    } else {
                        BrpLevel::None
                    }
                } else {
                    CargoDetector::file_brp_level(&target.source)
                };
                EnrichedTarget { target, brp_level }
            })
            .collect()
    }

    pub(super) fn create_unique_key(item: &EnrichedTarget) -> String {
        format!(
            "{}::{}::{}",
            item.target.manifest.display(),
            item.target.name,
            item.target.target_type.as_ref()
        )
    }

    pub(super) fn get_path_for_relative(item: &EnrichedTarget) -> PathBuf {
        item.target
            .manifest
            .parent()
            .unwrap_or(&item.target.manifest)
            .to_path_buf()
    }

    pub(super) fn serialize_item(item: &EnrichedTarget, relative_path: String) -> Value {
        json!({
            NAME_FIELD: item.target.name,
            KIND_FIELD: item.target.target_type.as_ref(),
            PACKAGE_NAME_FIELD: item.target.package_name,
            BRP_LEVEL_FIELD: item.brp_level.as_str(),
            WORKSPACE_ROOT_FIELD: item.target.workspace_root.display().to_string(),
            MANIFEST_PATH_FIELD: item.target.manifest.display().to_string(),
            // The relative_path field is designed for round-trip compatibility with launch functions.
            // This path can be used directly in `brp_launch`'s path parameter
            // to disambiguate between targets with the same name in different locations.
            RELATIVE_PATH_FIELD: relative_path,
            BUILDS_FIELD: create_builds_json(&item.target)
        })
    }
}

/// Collect all Bevy targets (apps and examples) with `kind` and `brp_enabled` fields
pub fn collect_all_bevy_targets(search_paths: &[PathBuf]) -> Vec<Value> {
    let mut all_items = Vec::new();
    let mut seen_items = HashSet::new();

    // Use the iterator to find all cargo projects
    for path in scanning::iter_cargo_project_paths(search_paths) {
        if let Ok(detector) = CargoDetector::try_from(path.as_path()) {
            let items = AllBevyTargetsStrategy::collect_items(&detector);
            for item in items {
                let key = AllBevyTargetsStrategy::create_unique_key(&item);
                if seen_items.insert(key) {
                    let item_path = AllBevyTargetsStrategy::get_path_for_relative(&item);
                    let relative_path = scanning::compute_relative_path(&item_path, search_paths);

                    let serialized_item = AllBevyTargetsStrategy::serialize_item(
                        &item,
                        relative_path.display().to_string(),
                    );
                    all_items.push(serialized_item);
                }
            }
        }
    }

    all_items
}

/// Helper function to create builds JSON for binary items
fn create_builds_json(item: &BevyTarget) -> Value {
    let profiles = vec![PROFILE_DEBUG, PROFILE_RELEASE];
    let mut builds = json!({});
    for profile in &profiles {
        let binary_path = item.get_binary_path(profile);
        builds[profile] = json!({
            PATH_FIELD: binary_path.display().to_string(),
            BUILD_BUILT_FIELD: binary_path.exists()
        });
    }
    builds
}