nex-pkg 0.25.3

Package manager UX for nix-darwin + homebrew
Documentation
use std::path::Path;

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

use crate::profile_fragment::{
    find_fragment_files, infer_fragment_path_id, ProfileFragmentDocument,
};

pub fn run_validate(path: &Path) -> Result<()> {
    if path.is_dir() {
        let files = find_fragment_files(path)?;
        let mut count = 0usize;
        for file in files {
            if validate_file(&file)? {
                count += 1;
            }
        }
        eprintln!("{} profile fragments valid", count);
    } else {
        ProfileFragmentDocument::from_path(path)?;
        eprintln!("{} is valid", path.display());
    }
    Ok(())
}

pub fn run_inspect(path: &Path, json: bool) -> Result<()> {
    if path.is_dir() {
        bail!("profile-fragment inspect expects a Pkl file or compatibility TOML file, not a directory");
    }
    let document = ProfileFragmentDocument::from_path(path)?;
    if json {
        println!(
            "{}",
            serde_json::to_string_pretty(&ProfileFragmentInspect::from(&document))?
        );
        return Ok(());
    }

    let fragment = &document.fragment;

    println!("\nProfile Fragment");
    print_kv("ID", &fragment.id);
    print_kv("Name", &fragment.name);
    print_kv("Version", &fragment.version);
    print_kv("Category", &fragment.category.to_string());
    print_kv(
        "Platforms",
        &fragment
            .platforms
            .iter()
            .map(ToString::to_string)
            .collect::<Vec<_>>()
            .join(", "),
    );
    if let Some(description) = &fragment.description {
        print_kv("Description", description);
    }
    if let Some(visibility) = &fragment.visibility {
        print_kv("Visibility", &visibility.to_string());
    }
    if !fragment.requires.is_empty() {
        print_kv("Requires", &fragment.requires.join(", "));
    }
    if !fragment.conflicts.is_empty() {
        print_kv("Conflicts", &fragment.conflicts.join(", "));
    }
    if let Some(safety) = &fragment.safety {
        println!("\nSafety");
        print_kv(
            "Mutates system services",
            &safety.mutates_system_services.to_string(),
        );
        print_kv(
            "Mutates hardware drivers",
            &safety.mutates_hardware_drivers.to_string(),
        );
        print_kv(
            "Requires confirmation",
            &safety.requires_confirmation.to_string(),
        );
    }

    Ok(())
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ProfileFragmentInspect {
    kind: &'static str,
    schema: String,
    id: String,
    name: String,
    version: String,
    description: Option<String>,
    category: String,
    requires: Vec<String>,
    conflicts: Vec<String>,
    platforms: Vec<String>,
    visibility: Option<String>,
    safety: Option<ProfileFragmentSafetyInspect>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ProfileFragmentSafetyInspect {
    mutates_system_services: bool,
    mutates_hardware_drivers: bool,
    requires_confirmation: bool,
}

impl From<&ProfileFragmentDocument> for ProfileFragmentInspect {
    fn from(document: &ProfileFragmentDocument) -> Self {
        let fragment = &document.fragment;
        Self {
            kind: "profile-fragment",
            schema: fragment.schema.clone(),
            id: fragment.id.clone(),
            name: fragment.name.clone(),
            version: fragment.version.clone(),
            description: fragment.description.clone(),
            category: fragment.category.to_string(),
            requires: fragment.requires.clone(),
            conflicts: fragment.conflicts.clone(),
            platforms: fragment.platforms.iter().map(ToString::to_string).collect(),
            visibility: fragment.visibility.as_ref().map(ToString::to_string),
            safety: fragment
                .safety
                .as_ref()
                .map(|safety| ProfileFragmentSafetyInspect {
                    mutates_system_services: safety.mutates_system_services,
                    mutates_hardware_drivers: safety.mutates_hardware_drivers,
                    requires_confirmation: safety.requires_confirmation,
                }),
        }
    }
}

fn validate_file(path: &Path) -> Result<bool> {
    let is_toml_fragment = if path.extension().and_then(|ext| ext.to_str()) == Some("toml") {
        std::fs::read_to_string(path)?.contains("[fragment]")
    } else {
        true
    };
    if !is_toml_fragment {
        return Ok(false);
    }
    let document = ProfileFragmentDocument::from_path(path)?;
    if let Some(path_id) = infer_fragment_path_id(path) {
        if document.fragment.id != path_id {
            bail!(
                "fragment id '{}' does not match path-derived id '{}' for {}",
                document.fragment.id,
                path_id,
                path.display()
            );
        }
    }
    Ok(true)
}

fn print_kv(key: &str, value: &str) {
    println!("  {key}: {value}");
}