knave 0.11.2

A small Lisp for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use super::exit_status::ExitStatus;
use super::project_file_edit::ProjectFileEdit;
use super::standard_error::StandardError;
use super::temporary_directory::TemporaryDirectory;
use super::terminal::Terminal;
use crate::build::{CrateResolver, ResolveError, ResolvedCrate};
use crate::project::{CrateDependency, CrateEntry, Section};

pub struct AddCommand {
    name: String,
    features: Vec<String>,
}

struct CrateNaming;

impl AddCommand {
    const COMMAND: &str = "add";

    pub fn new(name: String, features: Vec<String>) -> Self {
        Self { name, features }
    }

    pub fn run(self, terminal: &mut Terminal) -> ExitStatus {
        match self.add(terminal) {
            Ok(()) => ExitStatus::Success,
            Err(status) => status,
        }
    }

    fn add(&self, terminal: &Terminal) -> Result<(), ExitStatus> {
        self.check_names()?;
        let edit = ProjectFileEdit::of_working_directory(terminal, Self::COMMAND)?;
        Self::refuse_path(edit.listed_crate(&self.name))?;
        let resolved = self.resolved()?;
        let listed = edit.listed_crate(&resolved.name);
        Self::refuse_path(listed)?;
        let entry = CrateEntry {
            features: self.features_with(listed.map(|dependency| dependency.features.clone())),
            name: resolved.name,
            version: resolved.version,
        };
        edit.apply(terminal, |editor| {
            Some(editor.with_crate(Section::Dependencies, &entry))
        })?;
        Ok(())
    }

    fn check_names(&self) -> Result<(), ExitStatus> {
        if !CrateNaming::is_crate_name(&self.name) {
            return Err(Self::report_usage(&format!(
                "`{}` is not a crate name",
                self.name
            )));
        }
        match self
            .features
            .iter()
            .find(|feature| !CrateNaming::is_feature_name(feature))
        {
            Some(feature) => Err(Self::report_usage(&format!(
                "`{feature}` is not a feature name"
            ))),
            None => Ok(()),
        }
    }

    fn refuse_path(listed: Option<&CrateDependency>) -> Result<(), ExitStatus> {
        let Some(dependency) = listed.filter(|dependency| dependency.path.is_some()) else {
            return Ok(());
        };
        Err(Self::report_usage(&format!(
            "`{name}` is listed with `:path`\nhelp: run `knave remove {name}` first to switch it to a version",
            name = dependency.name
        )))
    }

    fn features_with(&self, listed: Option<Vec<String>>) -> Vec<String> {
        let mut features = listed.unwrap_or_default();
        for feature in &self.features {
            if !features.contains(feature) {
                features.push(feature.clone());
            }
        }
        features
    }

    fn resolved(&self) -> Result<ResolvedCrate, ExitStatus> {
        TemporaryDirectory::create()
            .map_err(ResolveError::Unwritable)
            .and_then(|scratch| {
                CrateResolver::new(scratch.path().to_path_buf()).resolve(&self.name, &self.features)
            })
            .map_err(|error| match error {
                ResolveError::CargoMissing(error) => Self::report_environment(&format!(
                    "cannot run cargo, which finds the version of a crate to add: {error}"
                )),
                ResolveError::Unwritable(error) => Self::report_environment(&format!(
                    "cannot write a scratch manifest for cargo: {error}"
                )),
                ResolveError::Cargo(message) => Self::report_cargo(&message),
            })
    }

    fn report_usage(message: &str) -> ExitStatus {
        StandardError::write_line(format_args!("error: {message}"));
        ExitStatus::UsageError
    }

    fn report_cargo(message: &str) -> ExitStatus {
        StandardError::write_line(format_args!("{message}"));
        ExitStatus::Environment
    }

    fn report_environment(message: &str) -> ExitStatus {
        StandardError::write_line(format_args!("error: {message}"));
        ExitStatus::Environment
    }
}

impl CrateNaming {
    const NAME_MARKS: [char; 2] = ['-', '_'];
    const FEATURE_MARKS: [char; 4] = ['-', '_', '+', '.'];
    const DEPENDENCY_FEATURE: char = '/';
    const FEATURE_START: char = '_';

    fn is_crate_name(name: &str) -> bool {
        name.starts_with(|first: char| first.is_ascii_alphabetic())
            && name.chars().all(|character| {
                character.is_ascii_alphanumeric() || Self::NAME_MARKS.contains(&character)
            })
    }

    fn is_feature_name(feature: &str) -> bool {
        let parts: Vec<&str> = feature.split(Self::DEPENDENCY_FEATURE).collect();
        parts.len() <= 2 && parts.iter().all(|part| Self::is_feature_part(part))
    }

    fn is_feature_part(part: &str) -> bool {
        part.starts_with(|first: char| {
            first.is_ascii_alphanumeric() || first == Self::FEATURE_START
        }) && part.chars().all(|character| {
            character.is_ascii_alphanumeric() || Self::FEATURE_MARKS.contains(&character)
        })
    }
}